diff --git a/gen/iam/v1/chat.pb.go b/gen/iam/v1/chat.pb.go new file mode 100644 index 00000000..1aa7f39b --- /dev/null +++ b/gen/iam/v1/chat.pb.go @@ -0,0 +1,3472 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: iam/v1/chat.proto + +package iamv1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + v1 "github.com/mutugading/goapps-backend/gen/common/v1" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ConversationProto represents a chat conversation (direct or group). +type ConversationProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + AvatarUrl string `protobuf:"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + Participants []*ParticipantProto `protobuf:"bytes,5,rep,name=participants,proto3" json:"participants,omitempty"` + LastMessage *MessageProto `protobuf:"bytes,6,opt,name=last_message,json=lastMessage,proto3" json:"last_message,omitempty"` + UnreadCount int32 `protobuf:"varint,7,opt,name=unread_count,json=unreadCount,proto3" json:"unread_count,omitempty"` + CreatedAt string `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt string `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConversationProto) Reset() { + *x = ConversationProto{} + mi := &file_iam_v1_chat_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConversationProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConversationProto) ProtoMessage() {} + +func (x *ConversationProto) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConversationProto.ProtoReflect.Descriptor instead. +func (*ConversationProto) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{0} +} + +func (x *ConversationProto) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *ConversationProto) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ConversationProto) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ConversationProto) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +func (x *ConversationProto) GetParticipants() []*ParticipantProto { + if x != nil { + return x.Participants + } + return nil +} + +func (x *ConversationProto) GetLastMessage() *MessageProto { + if x != nil { + return x.LastMessage + } + return nil +} + +func (x *ConversationProto) GetUnreadCount() int32 { + if x != nil { + return x.UnreadCount + } + return 0 +} + +func (x *ConversationProto) GetCreatedAt() string { + if x != nil { + return x.CreatedAt + } + return "" +} + +func (x *ConversationProto) GetUpdatedAt() string { + if x != nil { + return x.UpdatedAt + } + return "" +} + +// ParticipantProto represents a user participating in a conversation. +type ParticipantProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + FullName string `protobuf:"bytes,3,opt,name=full_name,json=fullName,proto3" json:"full_name,omitempty"` + AvatarUrl string `protobuf:"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + Role string `protobuf:"bytes,5,opt,name=role,proto3" json:"role,omitempty"` + IsOnline bool `protobuf:"varint,6,opt,name=is_online,json=isOnline,proto3" json:"is_online,omitempty"` + JoinedAt string `protobuf:"bytes,7,opt,name=joined_at,json=joinedAt,proto3" json:"joined_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParticipantProto) Reset() { + *x = ParticipantProto{} + mi := &file_iam_v1_chat_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParticipantProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParticipantProto) ProtoMessage() {} + +func (x *ParticipantProto) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParticipantProto.ProtoReflect.Descriptor instead. +func (*ParticipantProto) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{1} +} + +func (x *ParticipantProto) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ParticipantProto) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *ParticipantProto) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *ParticipantProto) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +func (x *ParticipantProto) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *ParticipantProto) GetIsOnline() bool { + if x != nil { + return x.IsOnline + } + return false +} + +func (x *ParticipantProto) GetJoinedAt() string { + if x != nil { + return x.JoinedAt + } + return "" +} + +// MessageProto represents a single chat message with decrypted body. +type MessageProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + MessageId string `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + SenderUserId string `protobuf:"bytes,3,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"` + SenderName string `protobuf:"bytes,4,opt,name=sender_name,json=senderName,proto3" json:"sender_name,omitempty"` + Body string `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"` + IsEdited bool `protobuf:"varint,6,opt,name=is_edited,json=isEdited,proto3" json:"is_edited,omitempty"` + IsDeleted bool `protobuf:"varint,7,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` + ReplyToId string `protobuf:"bytes,8,opt,name=reply_to_id,json=replyToId,proto3" json:"reply_to_id,omitempty"` + ReadReceipts []*ReadReceiptProto `protobuf:"bytes,9,rep,name=read_receipts,json=readReceipts,proto3" json:"read_receipts,omitempty"` + CreatedAt string `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt string `protobuf:"bytes,11,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + Attachments []*AttachmentProto `protobuf:"bytes,12,rep,name=attachments,proto3" json:"attachments,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageProto) Reset() { + *x = MessageProto{} + mi := &file_iam_v1_chat_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageProto) ProtoMessage() {} + +func (x *MessageProto) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageProto.ProtoReflect.Descriptor instead. +func (*MessageProto) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{2} +} + +func (x *MessageProto) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *MessageProto) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *MessageProto) GetSenderUserId() string { + if x != nil { + return x.SenderUserId + } + return "" +} + +func (x *MessageProto) GetSenderName() string { + if x != nil { + return x.SenderName + } + return "" +} + +func (x *MessageProto) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *MessageProto) GetIsEdited() bool { + if x != nil { + return x.IsEdited + } + return false +} + +func (x *MessageProto) GetIsDeleted() bool { + if x != nil { + return x.IsDeleted + } + return false +} + +func (x *MessageProto) GetReplyToId() string { + if x != nil { + return x.ReplyToId + } + return "" +} + +func (x *MessageProto) GetReadReceipts() []*ReadReceiptProto { + if x != nil { + return x.ReadReceipts + } + return nil +} + +func (x *MessageProto) GetCreatedAt() string { + if x != nil { + return x.CreatedAt + } + return "" +} + +func (x *MessageProto) GetUpdatedAt() string { + if x != nil { + return x.UpdatedAt + } + return "" +} + +func (x *MessageProto) GetAttachments() []*AttachmentProto { + if x != nil { + return x.Attachments + } + return nil +} + +// AttachmentProto represents a file or image attached to a chat message. +type AttachmentProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + AttachmentId string `protobuf:"bytes,1,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"` + FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + FileUrl string `protobuf:"bytes,3,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"` + ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + FileSize int64 `protobuf:"varint,5,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"` + ThumbnailUrl string `protobuf:"bytes,6,opt,name=thumbnail_url,json=thumbnailUrl,proto3" json:"thumbnail_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachmentProto) Reset() { + *x = AttachmentProto{} + mi := &file_iam_v1_chat_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachmentProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachmentProto) ProtoMessage() {} + +func (x *AttachmentProto) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachmentProto.ProtoReflect.Descriptor instead. +func (*AttachmentProto) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{3} +} + +func (x *AttachmentProto) GetAttachmentId() string { + if x != nil { + return x.AttachmentId + } + return "" +} + +func (x *AttachmentProto) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *AttachmentProto) GetFileUrl() string { + if x != nil { + return x.FileUrl + } + return "" +} + +func (x *AttachmentProto) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *AttachmentProto) GetFileSize() int64 { + if x != nil { + return x.FileSize + } + return 0 +} + +func (x *AttachmentProto) GetThumbnailUrl() string { + if x != nil { + return x.ThumbnailUrl + } + return "" +} + +// ReadReceiptProto represents a read receipt for a message. +type ReadReceiptProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ReadAt string `protobuf:"bytes,2,opt,name=read_at,json=readAt,proto3" json:"read_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadReceiptProto) Reset() { + *x = ReadReceiptProto{} + mi := &file_iam_v1_chat_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadReceiptProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadReceiptProto) ProtoMessage() {} + +func (x *ReadReceiptProto) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadReceiptProto.ProtoReflect.Descriptor instead. +func (*ReadReceiptProto) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{4} +} + +func (x *ReadReceiptProto) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ReadReceiptProto) GetReadAt() string { + if x != nil { + return x.ReadAt + } + return "" +} + +// EditHistoryEntryProto represents a single edit history entry for a message. +type EditHistoryEntryProto struct { + state protoimpl.MessageState `protogen:"open.v1"` + HistoryId int64 `protobuf:"varint,1,opt,name=history_id,json=historyId,proto3" json:"history_id,omitempty"` + Body string `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + EditedBy string `protobuf:"bytes,3,opt,name=edited_by,json=editedBy,proto3" json:"edited_by,omitempty"` + EditedAt string `protobuf:"bytes,4,opt,name=edited_at,json=editedAt,proto3" json:"edited_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EditHistoryEntryProto) Reset() { + *x = EditHistoryEntryProto{} + mi := &file_iam_v1_chat_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EditHistoryEntryProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EditHistoryEntryProto) ProtoMessage() {} + +func (x *EditHistoryEntryProto) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EditHistoryEntryProto.ProtoReflect.Descriptor instead. +func (*EditHistoryEntryProto) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{5} +} + +func (x *EditHistoryEntryProto) GetHistoryId() int64 { + if x != nil { + return x.HistoryId + } + return 0 +} + +func (x *EditHistoryEntryProto) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *EditHistoryEntryProto) GetEditedBy() string { + if x != nil { + return x.EditedBy + } + return "" +} + +func (x *EditHistoryEntryProto) GetEditedAt() string { + if x != nil { + return x.EditedAt + } + return "" +} + +// CreateDirectConversationRequest creates a 1:1 direct conversation. +type CreateDirectConversationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUserId string `protobuf:"bytes,1,opt,name=peer_user_id,json=peerUserId,proto3" json:"peer_user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateDirectConversationRequest) Reset() { + *x = CreateDirectConversationRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateDirectConversationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateDirectConversationRequest) ProtoMessage() {} + +func (x *CreateDirectConversationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateDirectConversationRequest.ProtoReflect.Descriptor instead. +func (*CreateDirectConversationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateDirectConversationRequest) GetPeerUserId() string { + if x != nil { + return x.PeerUserId + } + return "" +} + +// CreateGroupConversationRequest creates a group conversation. +type CreateGroupConversationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + ParticipantIds []string `protobuf:"bytes,2,rep,name=participant_ids,json=participantIds,proto3" json:"participant_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateGroupConversationRequest) Reset() { + *x = CreateGroupConversationRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateGroupConversationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateGroupConversationRequest) ProtoMessage() {} + +func (x *CreateGroupConversationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateGroupConversationRequest.ProtoReflect.Descriptor instead. +func (*CreateGroupConversationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{7} +} + +func (x *CreateGroupConversationRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateGroupConversationRequest) GetParticipantIds() []string { + if x != nil { + return x.ParticipantIds + } + return nil +} + +// GetConversationRequest retrieves a conversation by ID. +type GetConversationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConversationRequest) Reset() { + *x = GetConversationRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConversationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConversationRequest) ProtoMessage() {} + +func (x *GetConversationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetConversationRequest.ProtoReflect.Descriptor instead. +func (*GetConversationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{8} +} + +func (x *GetConversationRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +// ListConversationsRequest lists conversations for the current user. +type ListConversationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + Search string `protobuf:"bytes,3,opt,name=search,proto3" json:"search,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListConversationsRequest) Reset() { + *x = ListConversationsRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListConversationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListConversationsRequest) ProtoMessage() {} + +func (x *ListConversationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListConversationsRequest.ProtoReflect.Descriptor instead. +func (*ListConversationsRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{9} +} + +func (x *ListConversationsRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListConversationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListConversationsRequest) GetSearch() string { + if x != nil { + return x.Search + } + return "" +} + +// UpdateGroupConversationRequest updates a group conversation. +type UpdateGroupConversationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + AvatarUrl string `protobuf:"bytes,3,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateGroupConversationRequest) Reset() { + *x = UpdateGroupConversationRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateGroupConversationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateGroupConversationRequest) ProtoMessage() {} + +func (x *UpdateGroupConversationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateGroupConversationRequest.ProtoReflect.Descriptor instead. +func (*UpdateGroupConversationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateGroupConversationRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *UpdateGroupConversationRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateGroupConversationRequest) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +// AddParticipantsRequest adds users to a group conversation. +type AddParticipantsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + UserIds []string `protobuf:"bytes,2,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddParticipantsRequest) Reset() { + *x = AddParticipantsRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddParticipantsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddParticipantsRequest) ProtoMessage() {} + +func (x *AddParticipantsRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddParticipantsRequest.ProtoReflect.Descriptor instead. +func (*AddParticipantsRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{11} +} + +func (x *AddParticipantsRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *AddParticipantsRequest) GetUserIds() []string { + if x != nil { + return x.UserIds + } + return nil +} + +// RemoveParticipantRequest removes a user from a group conversation. +type RemoveParticipantRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveParticipantRequest) Reset() { + *x = RemoveParticipantRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveParticipantRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveParticipantRequest) ProtoMessage() {} + +func (x *RemoveParticipantRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveParticipantRequest.ProtoReflect.Descriptor instead. +func (*RemoveParticipantRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{12} +} + +func (x *RemoveParticipantRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *RemoveParticipantRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +// LeaveConversationRequest allows the current user to leave a group conversation. +type LeaveConversationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LeaveConversationRequest) Reset() { + *x = LeaveConversationRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LeaveConversationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LeaveConversationRequest) ProtoMessage() {} + +func (x *LeaveConversationRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LeaveConversationRequest.ProtoReflect.Descriptor instead. +func (*LeaveConversationRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{13} +} + +func (x *LeaveConversationRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +// SendMessageRequest sends a message to a conversation. +type SendMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + Body string `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + // ID of the message being replied to (optional, empty = no reply). + ReplyToId string `protobuf:"bytes,3,opt,name=reply_to_id,json=replyToId,proto3" json:"reply_to_id,omitempty"` + // Pre-uploaded attachment IDs to associate with this message (optional). + AttachmentIds []string `protobuf:"bytes,4,rep,name=attachment_ids,json=attachmentIds,proto3" json:"attachment_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendMessageRequest) Reset() { + *x = SendMessageRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendMessageRequest) ProtoMessage() {} + +func (x *SendMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendMessageRequest.ProtoReflect.Descriptor instead. +func (*SendMessageRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{14} +} + +func (x *SendMessageRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *SendMessageRequest) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *SendMessageRequest) GetReplyToId() string { + if x != nil { + return x.ReplyToId + } + return "" +} + +func (x *SendMessageRequest) GetAttachmentIds() []string { + if x != nil { + return x.AttachmentIds + } + return nil +} + +// EditMessageRequest edits an existing message. +type EditMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EditMessageRequest) Reset() { + *x = EditMessageRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EditMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EditMessageRequest) ProtoMessage() {} + +func (x *EditMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EditMessageRequest.ProtoReflect.Descriptor instead. +func (*EditMessageRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{15} +} + +func (x *EditMessageRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *EditMessageRequest) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *EditMessageRequest) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +// DeleteMessageRequest soft-deletes a message. +type DeleteMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMessageRequest) Reset() { + *x = DeleteMessageRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMessageRequest) ProtoMessage() {} + +func (x *DeleteMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMessageRequest.ProtoReflect.Descriptor instead. +func (*DeleteMessageRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{16} +} + +func (x *DeleteMessageRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *DeleteMessageRequest) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +// ListMessagesRequest lists messages in a conversation with cursor-based pagination. +type ListMessagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque base64 cursor; empty = start from latest messages. + BeforeCursor string `protobuf:"bytes,3,opt,name=before_cursor,json=beforeCursor,proto3" json:"before_cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMessagesRequest) Reset() { + *x = ListMessagesRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMessagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMessagesRequest) ProtoMessage() {} + +func (x *ListMessagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMessagesRequest.ProtoReflect.Descriptor instead. +func (*ListMessagesRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{17} +} + +func (x *ListMessagesRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *ListMessagesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListMessagesRequest) GetBeforeCursor() string { + if x != nil { + return x.BeforeCursor + } + return "" +} + +// StreamChatEventsRequest opens a server-streaming connection for chat events. +type StreamChatEventsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Last event ID seen by the client; used to resume the stream after reconnect. + LastEventId string `protobuf:"bytes,1,opt,name=last_event_id,json=lastEventId,proto3" json:"last_event_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamChatEventsRequest) Reset() { + *x = StreamChatEventsRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamChatEventsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamChatEventsRequest) ProtoMessage() {} + +func (x *StreamChatEventsRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamChatEventsRequest.ProtoReflect.Descriptor instead. +func (*StreamChatEventsRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{18} +} + +func (x *StreamChatEventsRequest) GetLastEventId() string { + if x != nil { + return x.LastEventId + } + return "" +} + +// MarkConversationReadRequest marks a conversation as read. +type MarkConversationReadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarkConversationReadRequest) Reset() { + *x = MarkConversationReadRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarkConversationReadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarkConversationReadRequest) ProtoMessage() {} + +func (x *MarkConversationReadRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarkConversationReadRequest.ProtoReflect.Descriptor instead. +func (*MarkConversationReadRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{19} +} + +func (x *MarkConversationReadRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +// ClearConversationHistoryRequest clears the calling user's message history for a conversation. +type ClearConversationHistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearConversationHistoryRequest) Reset() { + *x = ClearConversationHistoryRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearConversationHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearConversationHistoryRequest) ProtoMessage() {} + +func (x *ClearConversationHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearConversationHistoryRequest.ProtoReflect.Descriptor instead. +func (*ClearConversationHistoryRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{20} +} + +func (x *ClearConversationHistoryRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +// SetTypingRequest sets the typing indicator for the current user. +type SetTypingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + IsTyping bool `protobuf:"varint,2,opt,name=is_typing,json=isTyping,proto3" json:"is_typing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetTypingRequest) Reset() { + *x = SetTypingRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetTypingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTypingRequest) ProtoMessage() {} + +func (x *SetTypingRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetTypingRequest.ProtoReflect.Descriptor instead. +func (*SetTypingRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{21} +} + +func (x *SetTypingRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *SetTypingRequest) GetIsTyping() bool { + if x != nil { + return x.IsTyping + } + return false +} + +// GetMessageEditHistoryRequest retrieves the edit history for a message. +type GetMessageEditHistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMessageEditHistoryRequest) Reset() { + *x = GetMessageEditHistoryRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMessageEditHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMessageEditHistoryRequest) ProtoMessage() {} + +func (x *GetMessageEditHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMessageEditHistoryRequest.ProtoReflect.Descriptor instead. +func (*GetMessageEditHistoryRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{22} +} + +func (x *GetMessageEditHistoryRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *GetMessageEditHistoryRequest) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +// UploadChatAttachmentRequest uploads a single file to a conversation. +type UploadChatAttachmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + ContentType string `protobuf:"bytes,3,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + // Raw file bytes (max 25MB). + FileData []byte `protobuf:"bytes,4,opt,name=file_data,json=fileData,proto3" json:"file_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UploadChatAttachmentRequest) Reset() { + *x = UploadChatAttachmentRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UploadChatAttachmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadChatAttachmentRequest) ProtoMessage() {} + +func (x *UploadChatAttachmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadChatAttachmentRequest.ProtoReflect.Descriptor instead. +func (*UploadChatAttachmentRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{23} +} + +func (x *UploadChatAttachmentRequest) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *UploadChatAttachmentRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *UploadChatAttachmentRequest) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *UploadChatAttachmentRequest) GetFileData() []byte { + if x != nil { + return x.FileData + } + return nil +} + +// HeartbeatRequest sends a heartbeat to update online presence. +type HeartbeatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatRequest) Reset() { + *x = HeartbeatRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatRequest) ProtoMessage() {} + +func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{24} +} + +// GetOnlineUsersRequest retrieves online user IDs. +type GetOnlineUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserIds []string `protobuf:"bytes,1,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOnlineUsersRequest) Reset() { + *x = GetOnlineUsersRequest{} + mi := &file_iam_v1_chat_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOnlineUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOnlineUsersRequest) ProtoMessage() {} + +func (x *GetOnlineUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOnlineUsersRequest.ProtoReflect.Descriptor instead. +func (*GetOnlineUsersRequest) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{25} +} + +func (x *GetOnlineUsersRequest) GetUserIds() []string { + if x != nil { + return x.UserIds + } + return nil +} + +// CreateDirectConversationResponse returns the created direct conversation. +type CreateDirectConversationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *ConversationProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateDirectConversationResponse) Reset() { + *x = CreateDirectConversationResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateDirectConversationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateDirectConversationResponse) ProtoMessage() {} + +func (x *CreateDirectConversationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateDirectConversationResponse.ProtoReflect.Descriptor instead. +func (*CreateDirectConversationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{26} +} + +func (x *CreateDirectConversationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *CreateDirectConversationResponse) GetData() *ConversationProto { + if x != nil { + return x.Data + } + return nil +} + +// CreateGroupConversationResponse returns the created group conversation. +type CreateGroupConversationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *ConversationProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateGroupConversationResponse) Reset() { + *x = CreateGroupConversationResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateGroupConversationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateGroupConversationResponse) ProtoMessage() {} + +func (x *CreateGroupConversationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateGroupConversationResponse.ProtoReflect.Descriptor instead. +func (*CreateGroupConversationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{27} +} + +func (x *CreateGroupConversationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *CreateGroupConversationResponse) GetData() *ConversationProto { + if x != nil { + return x.Data + } + return nil +} + +// GetConversationResponse returns a single conversation. +type GetConversationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *ConversationProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConversationResponse) Reset() { + *x = GetConversationResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConversationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConversationResponse) ProtoMessage() {} + +func (x *GetConversationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetConversationResponse.ProtoReflect.Descriptor instead. +func (*GetConversationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{28} +} + +func (x *GetConversationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetConversationResponse) GetData() *ConversationProto { + if x != nil { + return x.Data + } + return nil +} + +// ListConversationsResponse returns a paginated list of conversations. +type ListConversationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data []*ConversationProto `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + Pagination *v1.PaginationResponse `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListConversationsResponse) Reset() { + *x = ListConversationsResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListConversationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListConversationsResponse) ProtoMessage() {} + +func (x *ListConversationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListConversationsResponse.ProtoReflect.Descriptor instead. +func (*ListConversationsResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{29} +} + +func (x *ListConversationsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListConversationsResponse) GetData() []*ConversationProto { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListConversationsResponse) GetPagination() *v1.PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +// UpdateGroupConversationResponse returns the updated group conversation. +type UpdateGroupConversationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *ConversationProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateGroupConversationResponse) Reset() { + *x = UpdateGroupConversationResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateGroupConversationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateGroupConversationResponse) ProtoMessage() {} + +func (x *UpdateGroupConversationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateGroupConversationResponse.ProtoReflect.Descriptor instead. +func (*UpdateGroupConversationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{30} +} + +func (x *UpdateGroupConversationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *UpdateGroupConversationResponse) GetData() *ConversationProto { + if x != nil { + return x.Data + } + return nil +} + +// AddParticipantsResponse confirms participants were added. +type AddParticipantsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddParticipantsResponse) Reset() { + *x = AddParticipantsResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddParticipantsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddParticipantsResponse) ProtoMessage() {} + +func (x *AddParticipantsResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddParticipantsResponse.ProtoReflect.Descriptor instead. +func (*AddParticipantsResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{31} +} + +func (x *AddParticipantsResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// RemoveParticipantResponse confirms a participant was removed. +type RemoveParticipantResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveParticipantResponse) Reset() { + *x = RemoveParticipantResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveParticipantResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveParticipantResponse) ProtoMessage() {} + +func (x *RemoveParticipantResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveParticipantResponse.ProtoReflect.Descriptor instead. +func (*RemoveParticipantResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{32} +} + +func (x *RemoveParticipantResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// LeaveConversationResponse confirms the user left the conversation. +type LeaveConversationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LeaveConversationResponse) Reset() { + *x = LeaveConversationResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LeaveConversationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LeaveConversationResponse) ProtoMessage() {} + +func (x *LeaveConversationResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LeaveConversationResponse.ProtoReflect.Descriptor instead. +func (*LeaveConversationResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{33} +} + +func (x *LeaveConversationResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// SendMessageResponse returns the sent message. +type SendMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *MessageProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendMessageResponse) Reset() { + *x = SendMessageResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendMessageResponse) ProtoMessage() {} + +func (x *SendMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendMessageResponse.ProtoReflect.Descriptor instead. +func (*SendMessageResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{34} +} + +func (x *SendMessageResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *SendMessageResponse) GetData() *MessageProto { + if x != nil { + return x.Data + } + return nil +} + +// EditMessageResponse returns the edited message. +type EditMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *MessageProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EditMessageResponse) Reset() { + *x = EditMessageResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EditMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EditMessageResponse) ProtoMessage() {} + +func (x *EditMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EditMessageResponse.ProtoReflect.Descriptor instead. +func (*EditMessageResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{35} +} + +func (x *EditMessageResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *EditMessageResponse) GetData() *MessageProto { + if x != nil { + return x.Data + } + return nil +} + +// DeleteMessageResponse confirms a message was deleted. +type DeleteMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMessageResponse) Reset() { + *x = DeleteMessageResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMessageResponse) ProtoMessage() {} + +func (x *DeleteMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMessageResponse.ProtoReflect.Descriptor instead. +func (*DeleteMessageResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{36} +} + +func (x *DeleteMessageResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// ListMessagesResponse returns a cursor-paginated list of messages. +type ListMessagesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data []*MessageProto `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + NextCursor string `protobuf:"bytes,3,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"` + HasMore bool `protobuf:"varint,4,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMessagesResponse) Reset() { + *x = ListMessagesResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMessagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMessagesResponse) ProtoMessage() {} + +func (x *ListMessagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMessagesResponse.ProtoReflect.Descriptor instead. +func (*ListMessagesResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{37} +} + +func (x *ListMessagesResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *ListMessagesResponse) GetData() []*MessageProto { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListMessagesResponse) GetNextCursor() string { + if x != nil { + return x.NextCursor + } + return "" +} + +func (x *ListMessagesResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +// MarkConversationReadResponse confirms the conversation was marked as read. +type MarkConversationReadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarkConversationReadResponse) Reset() { + *x = MarkConversationReadResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarkConversationReadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarkConversationReadResponse) ProtoMessage() {} + +func (x *MarkConversationReadResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarkConversationReadResponse.ProtoReflect.Descriptor instead. +func (*MarkConversationReadResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{38} +} + +func (x *MarkConversationReadResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// ClearConversationHistoryResponse confirms the caller's history was cleared. +type ClearConversationHistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearConversationHistoryResponse) Reset() { + *x = ClearConversationHistoryResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearConversationHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearConversationHistoryResponse) ProtoMessage() {} + +func (x *ClearConversationHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearConversationHistoryResponse.ProtoReflect.Descriptor instead. +func (*ClearConversationHistoryResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{39} +} + +func (x *ClearConversationHistoryResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// SetTypingResponse confirms the typing indicator was set. +type SetTypingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetTypingResponse) Reset() { + *x = SetTypingResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetTypingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTypingResponse) ProtoMessage() {} + +func (x *SetTypingResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetTypingResponse.ProtoReflect.Descriptor instead. +func (*SetTypingResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{40} +} + +func (x *SetTypingResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// GetMessageEditHistoryResponse returns the edit history for a message. +type GetMessageEditHistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data []*EditHistoryEntryProto `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMessageEditHistoryResponse) Reset() { + *x = GetMessageEditHistoryResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMessageEditHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMessageEditHistoryResponse) ProtoMessage() {} + +func (x *GetMessageEditHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMessageEditHistoryResponse.ProtoReflect.Descriptor instead. +func (*GetMessageEditHistoryResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{41} +} + +func (x *GetMessageEditHistoryResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetMessageEditHistoryResponse) GetData() []*EditHistoryEntryProto { + if x != nil { + return x.Data + } + return nil +} + +// UploadChatAttachmentResponse returns the uploaded attachment metadata. +type UploadChatAttachmentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + Data *AttachmentProto `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UploadChatAttachmentResponse) Reset() { + *x = UploadChatAttachmentResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UploadChatAttachmentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadChatAttachmentResponse) ProtoMessage() {} + +func (x *UploadChatAttachmentResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadChatAttachmentResponse.ProtoReflect.Descriptor instead. +func (*UploadChatAttachmentResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{42} +} + +func (x *UploadChatAttachmentResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *UploadChatAttachmentResponse) GetData() *AttachmentProto { + if x != nil { + return x.Data + } + return nil +} + +// HeartbeatResponse confirms the heartbeat was received. +type HeartbeatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatResponse) Reset() { + *x = HeartbeatResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatResponse) ProtoMessage() {} + +func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{43} +} + +func (x *HeartbeatResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +// GetOnlineUsersResponse returns the list of online user IDs. +type GetOnlineUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Base *v1.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` + UserIds []string `protobuf:"bytes,2,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOnlineUsersResponse) Reset() { + *x = GetOnlineUsersResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOnlineUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOnlineUsersResponse) ProtoMessage() {} + +func (x *GetOnlineUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOnlineUsersResponse.ProtoReflect.Descriptor instead. +func (*GetOnlineUsersResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{44} +} + +func (x *GetOnlineUsersResponse) GetBase() *v1.BaseResponse { + if x != nil { + return x.Base + } + return nil +} + +func (x *GetOnlineUsersResponse) GetUserIds() []string { + if x != nil { + return x.UserIds + } + return nil +} + +// StreamChatEventsResponse wraps a real-time chat event delivered via server streaming. +type StreamChatEventsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + // Types that are valid to be assigned to Payload: + // + // *StreamChatEventsResponse_MessageReceived + // *StreamChatEventsResponse_MessageEdited + // *StreamChatEventsResponse_MessageDeleted + // *StreamChatEventsResponse_Typing + // *StreamChatEventsResponse_ReadReceipt + // *StreamChatEventsResponse_Presence + Payload isStreamChatEventsResponse_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamChatEventsResponse) Reset() { + *x = StreamChatEventsResponse{} + mi := &file_iam_v1_chat_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamChatEventsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamChatEventsResponse) ProtoMessage() {} + +func (x *StreamChatEventsResponse) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamChatEventsResponse.ProtoReflect.Descriptor instead. +func (*StreamChatEventsResponse) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{45} +} + +func (x *StreamChatEventsResponse) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + +func (x *StreamChatEventsResponse) GetPayload() isStreamChatEventsResponse_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *StreamChatEventsResponse) GetMessageReceived() *MessageEvent { + if x != nil { + if x, ok := x.Payload.(*StreamChatEventsResponse_MessageReceived); ok { + return x.MessageReceived + } + } + return nil +} + +func (x *StreamChatEventsResponse) GetMessageEdited() *MessageEvent { + if x != nil { + if x, ok := x.Payload.(*StreamChatEventsResponse_MessageEdited); ok { + return x.MessageEdited + } + } + return nil +} + +func (x *StreamChatEventsResponse) GetMessageDeleted() *DeleteEvent { + if x != nil { + if x, ok := x.Payload.(*StreamChatEventsResponse_MessageDeleted); ok { + return x.MessageDeleted + } + } + return nil +} + +func (x *StreamChatEventsResponse) GetTyping() *TypingEvent { + if x != nil { + if x, ok := x.Payload.(*StreamChatEventsResponse_Typing); ok { + return x.Typing + } + } + return nil +} + +func (x *StreamChatEventsResponse) GetReadReceipt() *ReadEvent { + if x != nil { + if x, ok := x.Payload.(*StreamChatEventsResponse_ReadReceipt); ok { + return x.ReadReceipt + } + } + return nil +} + +func (x *StreamChatEventsResponse) GetPresence() *PresenceEvent { + if x != nil { + if x, ok := x.Payload.(*StreamChatEventsResponse_Presence); ok { + return x.Presence + } + } + return nil +} + +type isStreamChatEventsResponse_Payload interface { + isStreamChatEventsResponse_Payload() +} + +type StreamChatEventsResponse_MessageReceived struct { + MessageReceived *MessageEvent `protobuf:"bytes,2,opt,name=message_received,json=messageReceived,proto3,oneof"` +} + +type StreamChatEventsResponse_MessageEdited struct { + MessageEdited *MessageEvent `protobuf:"bytes,3,opt,name=message_edited,json=messageEdited,proto3,oneof"` +} + +type StreamChatEventsResponse_MessageDeleted struct { + MessageDeleted *DeleteEvent `protobuf:"bytes,4,opt,name=message_deleted,json=messageDeleted,proto3,oneof"` +} + +type StreamChatEventsResponse_Typing struct { + Typing *TypingEvent `protobuf:"bytes,5,opt,name=typing,proto3,oneof"` +} + +type StreamChatEventsResponse_ReadReceipt struct { + ReadReceipt *ReadEvent `protobuf:"bytes,6,opt,name=read_receipt,json=readReceipt,proto3,oneof"` +} + +type StreamChatEventsResponse_Presence struct { + Presence *PresenceEvent `protobuf:"bytes,7,opt,name=presence,proto3,oneof"` +} + +func (*StreamChatEventsResponse_MessageReceived) isStreamChatEventsResponse_Payload() {} + +func (*StreamChatEventsResponse_MessageEdited) isStreamChatEventsResponse_Payload() {} + +func (*StreamChatEventsResponse_MessageDeleted) isStreamChatEventsResponse_Payload() {} + +func (*StreamChatEventsResponse_Typing) isStreamChatEventsResponse_Payload() {} + +func (*StreamChatEventsResponse_ReadReceipt) isStreamChatEventsResponse_Payload() {} + +func (*StreamChatEventsResponse_Presence) isStreamChatEventsResponse_Payload() {} + +// MessageEvent represents a new or edited message event. +type MessageEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + Message *MessageProto `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageEvent) Reset() { + *x = MessageEvent{} + mi := &file_iam_v1_chat_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageEvent) ProtoMessage() {} + +func (x *MessageEvent) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageEvent.ProtoReflect.Descriptor instead. +func (*MessageEvent) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{46} +} + +func (x *MessageEvent) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *MessageEvent) GetMessage() *MessageProto { + if x != nil { + return x.Message + } + return nil +} + +// DeleteEvent represents a message deletion event. +type DeleteEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteEvent) Reset() { + *x = DeleteEvent{} + mi := &file_iam_v1_chat_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteEvent) ProtoMessage() {} + +func (x *DeleteEvent) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteEvent.ProtoReflect.Descriptor instead. +func (*DeleteEvent) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{47} +} + +func (x *DeleteEvent) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *DeleteEvent) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +// TypingEvent represents a typing indicator event. +type TypingEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + UserName string `protobuf:"bytes,3,opt,name=user_name,json=userName,proto3" json:"user_name,omitempty"` + IsTyping bool `protobuf:"varint,4,opt,name=is_typing,json=isTyping,proto3" json:"is_typing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TypingEvent) Reset() { + *x = TypingEvent{} + mi := &file_iam_v1_chat_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TypingEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TypingEvent) ProtoMessage() {} + +func (x *TypingEvent) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TypingEvent.ProtoReflect.Descriptor instead. +func (*TypingEvent) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{48} +} + +func (x *TypingEvent) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *TypingEvent) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *TypingEvent) GetUserName() string { + if x != nil { + return x.UserName + } + return "" +} + +func (x *TypingEvent) GetIsTyping() bool { + if x != nil { + return x.IsTyping + } + return false +} + +// ReadEvent represents a read receipt event. +type ReadEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + UserId string `protobuf:"bytes,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ReadAt string `protobuf:"bytes,4,opt,name=read_at,json=readAt,proto3" json:"read_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadEvent) Reset() { + *x = ReadEvent{} + mi := &file_iam_v1_chat_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadEvent) ProtoMessage() {} + +func (x *ReadEvent) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadEvent.ProtoReflect.Descriptor instead. +func (*ReadEvent) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{49} +} + +func (x *ReadEvent) GetConversationId() string { + if x != nil { + return x.ConversationId + } + return "" +} + +func (x *ReadEvent) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *ReadEvent) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ReadEvent) GetReadAt() string { + if x != nil { + return x.ReadAt + } + return "" +} + +// PresenceEvent represents a user online/offline status change. +type PresenceEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + IsOnline bool `protobuf:"varint,2,opt,name=is_online,json=isOnline,proto3" json:"is_online,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PresenceEvent) Reset() { + *x = PresenceEvent{} + mi := &file_iam_v1_chat_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PresenceEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PresenceEvent) ProtoMessage() {} + +func (x *PresenceEvent) ProtoReflect() protoreflect.Message { + mi := &file_iam_v1_chat_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PresenceEvent.ProtoReflect.Descriptor instead. +func (*PresenceEvent) Descriptor() ([]byte, []int) { + return file_iam_v1_chat_proto_rawDescGZIP(), []int{50} +} + +func (x *PresenceEvent) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *PresenceEvent) GetIsOnline() bool { + if x != nil { + return x.IsOnline + } + return false +} + +var File_iam_v1_chat_proto protoreflect.FileDescriptor + +const file_iam_v1_chat_proto_rawDesc = "" + + "\n" + + "\x11iam/v1/chat.proto\x12\x06iam.v1\x1a\x1bbuf/validate/validate.proto\x1a\x16common/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xdb\x02\n" + + "\x11ConversationProto\x12'\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "avatar_url\x18\x04 \x01(\tR\tavatarUrl\x12<\n" + + "\fparticipants\x18\x05 \x03(\v2\x18.iam.v1.ParticipantProtoR\fparticipants\x127\n" + + "\flast_message\x18\x06 \x01(\v2\x14.iam.v1.MessageProtoR\vlastMessage\x12!\n" + + "\funread_count\x18\a \x01(\x05R\vunreadCount\x12\x1d\n" + + "\n" + + "created_at\x18\b \x01(\tR\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\t \x01(\tR\tupdatedAt\"\xd1\x01\n" + + "\x10ParticipantProto\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x1b\n" + + "\tfull_name\x18\x03 \x01(\tR\bfullName\x12\x1d\n" + + "\n" + + "avatar_url\x18\x04 \x01(\tR\tavatarUrl\x12\x12\n" + + "\x04role\x18\x05 \x01(\tR\x04role\x12\x1b\n" + + "\tis_online\x18\x06 \x01(\bR\bisOnline\x12\x1b\n" + + "\tjoined_at\x18\a \x01(\tR\bjoinedAt\"\xc5\x03\n" + + "\fMessageProto\x12\x1d\n" + + "\n" + + "message_id\x18\x01 \x01(\tR\tmessageId\x12'\n" + + "\x0fconversation_id\x18\x02 \x01(\tR\x0econversationId\x12$\n" + + "\x0esender_user_id\x18\x03 \x01(\tR\fsenderUserId\x12\x1f\n" + + "\vsender_name\x18\x04 \x01(\tR\n" + + "senderName\x12\x12\n" + + "\x04body\x18\x05 \x01(\tR\x04body\x12\x1b\n" + + "\tis_edited\x18\x06 \x01(\bR\bisEdited\x12\x1d\n" + + "\n" + + "is_deleted\x18\a \x01(\bR\tisDeleted\x12\x1e\n" + + "\vreply_to_id\x18\b \x01(\tR\treplyToId\x12=\n" + + "\rread_receipts\x18\t \x03(\v2\x18.iam.v1.ReadReceiptProtoR\freadReceipts\x12\x1d\n" + + "\n" + + "created_at\x18\n" + + " \x01(\tR\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\v \x01(\tR\tupdatedAt\x129\n" + + "\vattachments\x18\f \x03(\v2\x17.iam.v1.AttachmentProtoR\vattachments\"\xd3\x01\n" + + "\x0fAttachmentProto\x12#\n" + + "\rattachment_id\x18\x01 \x01(\tR\fattachmentId\x12\x1b\n" + + "\tfile_name\x18\x02 \x01(\tR\bfileName\x12\x19\n" + + "\bfile_url\x18\x03 \x01(\tR\afileUrl\x12!\n" + + "\fcontent_type\x18\x04 \x01(\tR\vcontentType\x12\x1b\n" + + "\tfile_size\x18\x05 \x01(\x03R\bfileSize\x12#\n" + + "\rthumbnail_url\x18\x06 \x01(\tR\fthumbnailUrl\"D\n" + + "\x10ReadReceiptProto\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x17\n" + + "\aread_at\x18\x02 \x01(\tR\x06readAt\"\x84\x01\n" + + "\x15EditHistoryEntryProto\x12\x1d\n" + + "\n" + + "history_id\x18\x01 \x01(\x03R\thistoryId\x12\x12\n" + + "\x04body\x18\x02 \x01(\tR\x04body\x12\x1b\n" + + "\tedited_by\x18\x03 \x01(\tR\beditedBy\x12\x1b\n" + + "\tedited_at\x18\x04 \x01(\tR\beditedAt\"M\n" + + "\x1fCreateDirectConversationRequest\x12*\n" + + "\fpeer_user_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\n" + + "peerUserId\"s\n" + + "\x1eCreateGroupConversationRequest\x12\x1e\n" + + "\x04name\x18\x01 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\xc8\x01R\x04name\x121\n" + + "\x0fparticipant_ids\x18\x02 \x03(\tB\b\xbaH\x05\x92\x01\x02\b\x01R\x0eparticipantIds\"K\n" + + "\x16GetConversationRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\"\x80\x01\n" + + "\x18ListConversationsRequest\x12\x1b\n" + + "\x04page\x18\x01 \x01(\x05B\a\xbaH\x04\x1a\x02(\x01R\x04page\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d(\x01R\bpageSize\x12\x1f\n" + + "\x06search\x18\x03 \x01(\tB\a\xbaH\x04r\x02\x18dR\x06search\"\x9a\x01\n" + + "\x1eUpdateGroupConversationRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12\x1c\n" + + "\x04name\x18\x02 \x01(\tB\b\xbaH\x05r\x03\x18\xc8\x01R\x04name\x12'\n" + + "\n" + + "avatar_url\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x10R\tavatarUrl\"p\n" + + "\x16AddParticipantsRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12#\n" + + "\buser_ids\x18\x02 \x03(\tB\b\xbaH\x05\x92\x01\x02\b\x01R\auserIds\"p\n" + + "\x18RemoveParticipantRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12!\n" + + "\auser_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\"M\n" + + "\x18LeaveConversationRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\"\xae\x01\n" + + "\x12SendMessageRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12\x1e\n" + + "\x04body\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x90NR\x04body\x12\x1e\n" + + "\vreply_to_id\x18\x03 \x01(\tR\treplyToId\x12%\n" + + "\x0eattachment_ids\x18\x04 \x03(\tR\rattachmentIds\"\x90\x01\n" + + "\x12EditMessageRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12'\n" + + "\n" + + "message_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tmessageId\x12\x1e\n" + + "\x04body\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x90NR\x04body\"r\n" + + "\x14DeleteMessageRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12'\n" + + "\n" + + "message_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tmessageId\"\x95\x01\n" + + "\x13ListMessagesRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d(\x01R\bpageSize\x12#\n" + + "\rbefore_cursor\x18\x03 \x01(\tR\fbeforeCursor\"=\n" + + "\x17StreamChatEventsRequest\x12\"\n" + + "\rlast_event_id\x18\x01 \x01(\tR\vlastEventId\"P\n" + + "\x1bMarkConversationReadRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\"T\n" + + "\x1fClearConversationHistoryRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\"b\n" + + "\x10SetTypingRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12\x1b\n" + + "\tis_typing\x18\x02 \x01(\bR\bisTyping\"z\n" + + "\x1cGetMessageEditHistoryRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12'\n" + + "\n" + + "message_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tmessageId\"\xd2\x01\n" + + "\x1bUploadChatAttachmentRequest\x121\n" + + "\x0fconversation_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0econversationId\x12'\n" + + "\tfile_name\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\xff\x01R\bfileName\x12,\n" + + "\fcontent_type\x18\x03 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\vcontentType\x12)\n" + + "\tfile_data\x18\x04 \x01(\fB\f\xbaH\tz\a\x10\x01\x18\x80\x80\xc0\fR\bfileData\"\x12\n" + + "\x10HeartbeatRequest\"2\n" + + "\x15GetOnlineUsersRequest\x12\x19\n" + + "\buser_ids\x18\x01 \x03(\tR\auserIds\"~\n" + + " CreateDirectConversationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x01(\v2\x19.iam.v1.ConversationProtoR\x04data\"}\n" + + "\x1fCreateGroupConversationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x01(\v2\x19.iam.v1.ConversationProtoR\x04data\"u\n" + + "\x17GetConversationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x01(\v2\x19.iam.v1.ConversationProtoR\x04data\"\xb6\x01\n" + + "\x19ListConversationsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x03(\v2\x19.iam.v1.ConversationProtoR\x04data\x12=\n" + + "\n" + + "pagination\x18\x03 \x01(\v2\x1d.common.v1.PaginationResponseR\n" + + "pagination\"}\n" + + "\x1fUpdateGroupConversationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12-\n" + + "\x04data\x18\x02 \x01(\v2\x19.iam.v1.ConversationProtoR\x04data\"F\n" + + "\x17AddParticipantsResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"H\n" + + "\x19RemoveParticipantResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"H\n" + + "\x19LeaveConversationResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"l\n" + + "\x13SendMessageResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12(\n" + + "\x04data\x18\x02 \x01(\v2\x14.iam.v1.MessageProtoR\x04data\"l\n" + + "\x13EditMessageResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12(\n" + + "\x04data\x18\x02 \x01(\v2\x14.iam.v1.MessageProtoR\x04data\"D\n" + + "\x15DeleteMessageResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"\xa9\x01\n" + + "\x14ListMessagesResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12(\n" + + "\x04data\x18\x02 \x03(\v2\x14.iam.v1.MessageProtoR\x04data\x12\x1f\n" + + "\vnext_cursor\x18\x03 \x01(\tR\n" + + "nextCursor\x12\x19\n" + + "\bhas_more\x18\x04 \x01(\bR\ahasMore\"K\n" + + "\x1cMarkConversationReadResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"O\n" + + " ClearConversationHistoryResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"@\n" + + "\x11SetTypingResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"\x7f\n" + + "\x1dGetMessageEditHistoryResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x121\n" + + "\x04data\x18\x02 \x03(\v2\x1d.iam.v1.EditHistoryEntryProtoR\x04data\"x\n" + + "\x1cUploadChatAttachmentResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12+\n" + + "\x04data\x18\x02 \x01(\v2\x17.iam.v1.AttachmentProtoR\x04data\"@\n" + + "\x11HeartbeatResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\"`\n" + + "\x16GetOnlineUsersResponse\x12+\n" + + "\x04base\x18\x01 \x01(\v2\x17.common.v1.BaseResponseR\x04base\x12\x19\n" + + "\buser_ids\x18\x02 \x03(\tR\auserIds\"\x9e\x03\n" + + "\x18StreamChatEventsResponse\x12\x19\n" + + "\bevent_id\x18\x01 \x01(\tR\aeventId\x12A\n" + + "\x10message_received\x18\x02 \x01(\v2\x14.iam.v1.MessageEventH\x00R\x0fmessageReceived\x12=\n" + + "\x0emessage_edited\x18\x03 \x01(\v2\x14.iam.v1.MessageEventH\x00R\rmessageEdited\x12>\n" + + "\x0fmessage_deleted\x18\x04 \x01(\v2\x13.iam.v1.DeleteEventH\x00R\x0emessageDeleted\x12-\n" + + "\x06typing\x18\x05 \x01(\v2\x13.iam.v1.TypingEventH\x00R\x06typing\x126\n" + + "\fread_receipt\x18\x06 \x01(\v2\x11.iam.v1.ReadEventH\x00R\vreadReceipt\x123\n" + + "\bpresence\x18\a \x01(\v2\x15.iam.v1.PresenceEventH\x00R\bpresenceB\t\n" + + "\apayload\"g\n" + + "\fMessageEvent\x12'\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12.\n" + + "\amessage\x18\x02 \x01(\v2\x14.iam.v1.MessageProtoR\amessage\"U\n" + + "\vDeleteEvent\x12'\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x1d\n" + + "\n" + + "message_id\x18\x02 \x01(\tR\tmessageId\"\x89\x01\n" + + "\vTypingEvent\x12'\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\tR\x06userId\x12\x1b\n" + + "\tuser_name\x18\x03 \x01(\tR\buserName\x12\x1b\n" + + "\tis_typing\x18\x04 \x01(\bR\bisTyping\"\x85\x01\n" + + "\tReadEvent\x12'\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x1d\n" + + "\n" + + "message_id\x18\x02 \x01(\tR\tmessageId\x12\x17\n" + + "\auser_id\x18\x03 \x01(\tR\x06userId\x12\x17\n" + + "\aread_at\x18\x04 \x01(\tR\x06readAt\"E\n" + + "\rPresenceEvent\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1b\n" + + "\tis_online\x18\x02 \x01(\bR\bisOnline2\x90\x16\n" + + "\vChatService\x12\x9f\x01\n" + + "\x18CreateDirectConversation\x12'.iam.v1.CreateDirectConversationRequest\x1a(.iam.v1.CreateDirectConversationResponse\"0\x82\xd3\xe4\x93\x02*:\x01*\"%/api/v1/iam/chat/conversations/direct\x12\x9b\x01\n" + + "\x17CreateGroupConversation\x12&.iam.v1.CreateGroupConversationRequest\x1a'.iam.v1.CreateGroupConversationResponse\"/\x82\xd3\xe4\x93\x02):\x01*\"$/api/v1/iam/chat/conversations/group\x12\x8c\x01\n" + + "\x0fGetConversation\x12\x1e.iam.v1.GetConversationRequest\x1a\x1f.iam.v1.GetConversationResponse\"8\x82\xd3\xe4\x93\x022\x120/api/v1/iam/chat/conversations/{conversation_id}\x12\x80\x01\n" + + "\x11ListConversations\x12 .iam.v1.ListConversationsRequest\x1a!.iam.v1.ListConversationsResponse\"&\x82\xd3\xe4\x93\x02 \x12\x1e/api/v1/iam/chat/conversations\x12\xa7\x01\n" + + "\x17UpdateGroupConversation\x12&.iam.v1.UpdateGroupConversationRequest\x1a'.iam.v1.UpdateGroupConversationResponse\";\x82\xd3\xe4\x93\x025:\x01*\x1a0/api/v1/iam/chat/conversations/{conversation_id}\x12\x9c\x01\n" + + "\x0fAddParticipants\x12\x1e.iam.v1.AddParticipantsRequest\x1a\x1f.iam.v1.AddParticipantsResponse\"H\x82\xd3\xe4\x93\x02B:\x01*\"=/api/v1/iam/chat/conversations/{conversation_id}/participants\x12\xa9\x01\n" + + "\x11RemoveParticipant\x12 .iam.v1.RemoveParticipantRequest\x1a!.iam.v1.RemoveParticipantResponse\"O\x82\xd3\xe4\x93\x02I*G/api/v1/iam/chat/conversations/{conversation_id}/participants/{user_id}\x12\x9b\x01\n" + + "\x11LeaveConversation\x12 .iam.v1.LeaveConversationRequest\x1a!.iam.v1.LeaveConversationResponse\"A\x82\xd3\xe4\x93\x02;:\x01*\"6/api/v1/iam/chat/conversations/{conversation_id}/leave\x12\x8c\x01\n" + + "\vSendMessage\x12\x1a.iam.v1.SendMessageRequest\x1a\x1b.iam.v1.SendMessageResponse\"D\x82\xd3\xe4\x93\x02>:\x01*\"9/api/v1/iam/chat/conversations/{conversation_id}/messages\x12\x99\x01\n" + + "\vEditMessage\x12\x1a.iam.v1.EditMessageRequest\x1a\x1b.iam.v1.EditMessageResponse\"Q\x82\xd3\xe4\x93\x02K:\x01*\x1aF/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}\x12\x9c\x01\n" + + "\rDeleteMessage\x12\x1c.iam.v1.DeleteMessageRequest\x1a\x1d.iam.v1.DeleteMessageResponse\"N\x82\xd3\xe4\x93\x02H*F/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}\x12\x8c\x01\n" + + "\fListMessages\x12\x1b.iam.v1.ListMessagesRequest\x1a\x1c.iam.v1.ListMessagesResponse\"A\x82\xd3\xe4\x93\x02;\x129/api/v1/iam/chat/conversations/{conversation_id}/messages\x12x\n" + + "\x10StreamChatEvents\x12\x1f.iam.v1.StreamChatEventsRequest\x1a .iam.v1.StreamChatEventsResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/api/v1/iam/chat/events0\x01\x12\xa3\x01\n" + + "\x14MarkConversationRead\x12#.iam.v1.MarkConversationReadRequest\x1a$.iam.v1.MarkConversationReadResponse\"@\x82\xd3\xe4\x93\x02::\x01*\"5/api/v1/iam/chat/conversations/{conversation_id}/read\x12\xaf\x01\n" + + "\x18ClearConversationHistory\x12'.iam.v1.ClearConversationHistoryRequest\x1a(.iam.v1.ClearConversationHistoryResponse\"@\x82\xd3\xe4\x93\x02:*8/api/v1/iam/chat/conversations/{conversation_id}/history\x12\x84\x01\n" + + "\tSetTyping\x12\x18.iam.v1.SetTypingRequest\x1a\x19.iam.v1.SetTypingResponse\"B\x82\xd3\xe4\x93\x02<:\x01*\"7/api/v1/iam/chat/conversations/{conversation_id}/typing\x12\xbc\x01\n" + + "\x15GetMessageEditHistory\x12$.iam.v1.GetMessageEditHistoryRequest\x1a%.iam.v1.GetMessageEditHistoryResponse\"V\x82\xd3\xe4\x93\x02P\x12N/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}/history\x12\xaa\x01\n" + + "\x14UploadChatAttachment\x12#.iam.v1.UploadChatAttachmentRequest\x1a$.iam.v1.UploadChatAttachmentResponse\"G\x82\xd3\xe4\x93\x02A:\x01*\" iam.v1.ParticipantProto + 2, // 1: iam.v1.ConversationProto.last_message:type_name -> iam.v1.MessageProto + 4, // 2: iam.v1.MessageProto.read_receipts:type_name -> iam.v1.ReadReceiptProto + 3, // 3: iam.v1.MessageProto.attachments:type_name -> iam.v1.AttachmentProto + 51, // 4: iam.v1.CreateDirectConversationResponse.base:type_name -> common.v1.BaseResponse + 0, // 5: iam.v1.CreateDirectConversationResponse.data:type_name -> iam.v1.ConversationProto + 51, // 6: iam.v1.CreateGroupConversationResponse.base:type_name -> common.v1.BaseResponse + 0, // 7: iam.v1.CreateGroupConversationResponse.data:type_name -> iam.v1.ConversationProto + 51, // 8: iam.v1.GetConversationResponse.base:type_name -> common.v1.BaseResponse + 0, // 9: iam.v1.GetConversationResponse.data:type_name -> iam.v1.ConversationProto + 51, // 10: iam.v1.ListConversationsResponse.base:type_name -> common.v1.BaseResponse + 0, // 11: iam.v1.ListConversationsResponse.data:type_name -> iam.v1.ConversationProto + 52, // 12: iam.v1.ListConversationsResponse.pagination:type_name -> common.v1.PaginationResponse + 51, // 13: iam.v1.UpdateGroupConversationResponse.base:type_name -> common.v1.BaseResponse + 0, // 14: iam.v1.UpdateGroupConversationResponse.data:type_name -> iam.v1.ConversationProto + 51, // 15: iam.v1.AddParticipantsResponse.base:type_name -> common.v1.BaseResponse + 51, // 16: iam.v1.RemoveParticipantResponse.base:type_name -> common.v1.BaseResponse + 51, // 17: iam.v1.LeaveConversationResponse.base:type_name -> common.v1.BaseResponse + 51, // 18: iam.v1.SendMessageResponse.base:type_name -> common.v1.BaseResponse + 2, // 19: iam.v1.SendMessageResponse.data:type_name -> iam.v1.MessageProto + 51, // 20: iam.v1.EditMessageResponse.base:type_name -> common.v1.BaseResponse + 2, // 21: iam.v1.EditMessageResponse.data:type_name -> iam.v1.MessageProto + 51, // 22: iam.v1.DeleteMessageResponse.base:type_name -> common.v1.BaseResponse + 51, // 23: iam.v1.ListMessagesResponse.base:type_name -> common.v1.BaseResponse + 2, // 24: iam.v1.ListMessagesResponse.data:type_name -> iam.v1.MessageProto + 51, // 25: iam.v1.MarkConversationReadResponse.base:type_name -> common.v1.BaseResponse + 51, // 26: iam.v1.ClearConversationHistoryResponse.base:type_name -> common.v1.BaseResponse + 51, // 27: iam.v1.SetTypingResponse.base:type_name -> common.v1.BaseResponse + 51, // 28: iam.v1.GetMessageEditHistoryResponse.base:type_name -> common.v1.BaseResponse + 5, // 29: iam.v1.GetMessageEditHistoryResponse.data:type_name -> iam.v1.EditHistoryEntryProto + 51, // 30: iam.v1.UploadChatAttachmentResponse.base:type_name -> common.v1.BaseResponse + 3, // 31: iam.v1.UploadChatAttachmentResponse.data:type_name -> iam.v1.AttachmentProto + 51, // 32: iam.v1.HeartbeatResponse.base:type_name -> common.v1.BaseResponse + 51, // 33: iam.v1.GetOnlineUsersResponse.base:type_name -> common.v1.BaseResponse + 46, // 34: iam.v1.StreamChatEventsResponse.message_received:type_name -> iam.v1.MessageEvent + 46, // 35: iam.v1.StreamChatEventsResponse.message_edited:type_name -> iam.v1.MessageEvent + 47, // 36: iam.v1.StreamChatEventsResponse.message_deleted:type_name -> iam.v1.DeleteEvent + 48, // 37: iam.v1.StreamChatEventsResponse.typing:type_name -> iam.v1.TypingEvent + 49, // 38: iam.v1.StreamChatEventsResponse.read_receipt:type_name -> iam.v1.ReadEvent + 50, // 39: iam.v1.StreamChatEventsResponse.presence:type_name -> iam.v1.PresenceEvent + 2, // 40: iam.v1.MessageEvent.message:type_name -> iam.v1.MessageProto + 6, // 41: iam.v1.ChatService.CreateDirectConversation:input_type -> iam.v1.CreateDirectConversationRequest + 7, // 42: iam.v1.ChatService.CreateGroupConversation:input_type -> iam.v1.CreateGroupConversationRequest + 8, // 43: iam.v1.ChatService.GetConversation:input_type -> iam.v1.GetConversationRequest + 9, // 44: iam.v1.ChatService.ListConversations:input_type -> iam.v1.ListConversationsRequest + 10, // 45: iam.v1.ChatService.UpdateGroupConversation:input_type -> iam.v1.UpdateGroupConversationRequest + 11, // 46: iam.v1.ChatService.AddParticipants:input_type -> iam.v1.AddParticipantsRequest + 12, // 47: iam.v1.ChatService.RemoveParticipant:input_type -> iam.v1.RemoveParticipantRequest + 13, // 48: iam.v1.ChatService.LeaveConversation:input_type -> iam.v1.LeaveConversationRequest + 14, // 49: iam.v1.ChatService.SendMessage:input_type -> iam.v1.SendMessageRequest + 15, // 50: iam.v1.ChatService.EditMessage:input_type -> iam.v1.EditMessageRequest + 16, // 51: iam.v1.ChatService.DeleteMessage:input_type -> iam.v1.DeleteMessageRequest + 17, // 52: iam.v1.ChatService.ListMessages:input_type -> iam.v1.ListMessagesRequest + 18, // 53: iam.v1.ChatService.StreamChatEvents:input_type -> iam.v1.StreamChatEventsRequest + 19, // 54: iam.v1.ChatService.MarkConversationRead:input_type -> iam.v1.MarkConversationReadRequest + 20, // 55: iam.v1.ChatService.ClearConversationHistory:input_type -> iam.v1.ClearConversationHistoryRequest + 21, // 56: iam.v1.ChatService.SetTyping:input_type -> iam.v1.SetTypingRequest + 22, // 57: iam.v1.ChatService.GetMessageEditHistory:input_type -> iam.v1.GetMessageEditHistoryRequest + 23, // 58: iam.v1.ChatService.UploadChatAttachment:input_type -> iam.v1.UploadChatAttachmentRequest + 24, // 59: iam.v1.PresenceService.Heartbeat:input_type -> iam.v1.HeartbeatRequest + 25, // 60: iam.v1.PresenceService.GetOnlineUsers:input_type -> iam.v1.GetOnlineUsersRequest + 26, // 61: iam.v1.ChatService.CreateDirectConversation:output_type -> iam.v1.CreateDirectConversationResponse + 27, // 62: iam.v1.ChatService.CreateGroupConversation:output_type -> iam.v1.CreateGroupConversationResponse + 28, // 63: iam.v1.ChatService.GetConversation:output_type -> iam.v1.GetConversationResponse + 29, // 64: iam.v1.ChatService.ListConversations:output_type -> iam.v1.ListConversationsResponse + 30, // 65: iam.v1.ChatService.UpdateGroupConversation:output_type -> iam.v1.UpdateGroupConversationResponse + 31, // 66: iam.v1.ChatService.AddParticipants:output_type -> iam.v1.AddParticipantsResponse + 32, // 67: iam.v1.ChatService.RemoveParticipant:output_type -> iam.v1.RemoveParticipantResponse + 33, // 68: iam.v1.ChatService.LeaveConversation:output_type -> iam.v1.LeaveConversationResponse + 34, // 69: iam.v1.ChatService.SendMessage:output_type -> iam.v1.SendMessageResponse + 35, // 70: iam.v1.ChatService.EditMessage:output_type -> iam.v1.EditMessageResponse + 36, // 71: iam.v1.ChatService.DeleteMessage:output_type -> iam.v1.DeleteMessageResponse + 37, // 72: iam.v1.ChatService.ListMessages:output_type -> iam.v1.ListMessagesResponse + 45, // 73: iam.v1.ChatService.StreamChatEvents:output_type -> iam.v1.StreamChatEventsResponse + 38, // 74: iam.v1.ChatService.MarkConversationRead:output_type -> iam.v1.MarkConversationReadResponse + 39, // 75: iam.v1.ChatService.ClearConversationHistory:output_type -> iam.v1.ClearConversationHistoryResponse + 40, // 76: iam.v1.ChatService.SetTyping:output_type -> iam.v1.SetTypingResponse + 41, // 77: iam.v1.ChatService.GetMessageEditHistory:output_type -> iam.v1.GetMessageEditHistoryResponse + 42, // 78: iam.v1.ChatService.UploadChatAttachment:output_type -> iam.v1.UploadChatAttachmentResponse + 43, // 79: iam.v1.PresenceService.Heartbeat:output_type -> iam.v1.HeartbeatResponse + 44, // 80: iam.v1.PresenceService.GetOnlineUsers:output_type -> iam.v1.GetOnlineUsersResponse + 61, // [61:81] is the sub-list for method output_type + 41, // [41:61] is the sub-list for method input_type + 41, // [41:41] is the sub-list for extension type_name + 41, // [41:41] is the sub-list for extension extendee + 0, // [0:41] is the sub-list for field type_name +} + +func init() { file_iam_v1_chat_proto_init() } +func file_iam_v1_chat_proto_init() { + if File_iam_v1_chat_proto != nil { + return + } + file_iam_v1_chat_proto_msgTypes[45].OneofWrappers = []any{ + (*StreamChatEventsResponse_MessageReceived)(nil), + (*StreamChatEventsResponse_MessageEdited)(nil), + (*StreamChatEventsResponse_MessageDeleted)(nil), + (*StreamChatEventsResponse_Typing)(nil), + (*StreamChatEventsResponse_ReadReceipt)(nil), + (*StreamChatEventsResponse_Presence)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_iam_v1_chat_proto_rawDesc), len(file_iam_v1_chat_proto_rawDesc)), + NumEnums: 0, + NumMessages: 51, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_iam_v1_chat_proto_goTypes, + DependencyIndexes: file_iam_v1_chat_proto_depIdxs, + MessageInfos: file_iam_v1_chat_proto_msgTypes, + }.Build() + File_iam_v1_chat_proto = out.File + file_iam_v1_chat_proto_goTypes = nil + file_iam_v1_chat_proto_depIdxs = nil +} diff --git a/gen/iam/v1/chat.pb.gw.go b/gen/iam/v1/chat.pb.gw.go new file mode 100644 index 00000000..268937e1 --- /dev/null +++ b/gen/iam/v1/chat.pb.gw.go @@ -0,0 +1,1764 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: iam/v1/chat.proto + +/* +Package iamv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package iamv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_ChatService_CreateDirectConversation_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateDirectConversationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateDirectConversation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_CreateDirectConversation_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateDirectConversationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateDirectConversation(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_CreateGroupConversation_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateGroupConversationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateGroupConversation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_CreateGroupConversation_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateGroupConversationRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateGroupConversation(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_GetConversation_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetConversationRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.GetConversation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_GetConversation_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetConversationRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.GetConversation(ctx, &protoReq) + return msg, metadata, err +} + +var filter_ChatService_ListConversations_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_ChatService_ListConversations_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListConversationsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_ListConversations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListConversations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_ListConversations_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListConversationsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_ListConversations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListConversations(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_UpdateGroupConversation_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateGroupConversationRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.UpdateGroupConversation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_UpdateGroupConversation_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateGroupConversationRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.UpdateGroupConversation(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_AddParticipants_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddParticipantsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.AddParticipants(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_AddParticipants_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddParticipantsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.AddParticipants(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_RemoveParticipant_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RemoveParticipantRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["user_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user_id") + } + protoReq.UserId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user_id", err) + } + msg, err := client.RemoveParticipant(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_RemoveParticipant_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RemoveParticipantRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["user_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user_id") + } + protoReq.UserId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user_id", err) + } + msg, err := server.RemoveParticipant(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_LeaveConversation_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq LeaveConversationRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.LeaveConversation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_LeaveConversation_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq LeaveConversationRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.LeaveConversation(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_SendMessage_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SendMessageRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.SendMessage(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_SendMessage_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SendMessageRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.SendMessage(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_EditMessage_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq EditMessageRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["message_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "message_id") + } + protoReq.MessageId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "message_id", err) + } + msg, err := client.EditMessage(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_EditMessage_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq EditMessageRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["message_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "message_id") + } + protoReq.MessageId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "message_id", err) + } + msg, err := server.EditMessage(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_DeleteMessage_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMessageRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["message_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "message_id") + } + protoReq.MessageId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "message_id", err) + } + msg, err := client.DeleteMessage(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_DeleteMessage_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMessageRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["message_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "message_id") + } + protoReq.MessageId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "message_id", err) + } + msg, err := server.DeleteMessage(ctx, &protoReq) + return msg, metadata, err +} + +var filter_ChatService_ListMessages_0 = &utilities.DoubleArray{Encoding: map[string]int{"conversation_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_ChatService_ListMessages_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMessagesRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_ListMessages_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMessages(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_ListMessages_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMessagesRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_ListMessages_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMessages(ctx, &protoReq) + return msg, metadata, err +} + +var filter_ChatService_StreamChatEvents_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_ChatService_StreamChatEvents_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (ChatService_StreamChatEventsClient, runtime.ServerMetadata, error) { + var ( + protoReq StreamChatEventsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_StreamChatEvents_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + stream, err := client.StreamChatEvents(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil +} + +func request_ChatService_MarkConversationRead_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq MarkConversationReadRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.MarkConversationRead(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_MarkConversationRead_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq MarkConversationReadRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.MarkConversationRead(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_ClearConversationHistory_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ClearConversationHistoryRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.ClearConversationHistory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_ClearConversationHistory_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ClearConversationHistoryRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.ClearConversationHistory(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_SetTyping_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SetTypingRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.SetTyping(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_SetTyping_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SetTypingRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.SetTyping(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_GetMessageEditHistory_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetMessageEditHistoryRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["message_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "message_id") + } + protoReq.MessageId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "message_id", err) + } + msg, err := client.GetMessageEditHistory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_GetMessageEditHistory_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetMessageEditHistoryRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + val, ok = pathParams["message_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "message_id") + } + protoReq.MessageId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "message_id", err) + } + msg, err := server.GetMessageEditHistory(ctx, &protoReq) + return msg, metadata, err +} + +func request_ChatService_UploadChatAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UploadChatAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := client.UploadChatAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ChatService_UploadChatAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server ChatServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UploadChatAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["conversation_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "conversation_id") + } + protoReq.ConversationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) + } + msg, err := server.UploadChatAttachment(ctx, &protoReq) + return msg, metadata, err +} + +func request_PresenceService_Heartbeat_0(ctx context.Context, marshaler runtime.Marshaler, client PresenceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq HeartbeatRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.Heartbeat(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_PresenceService_Heartbeat_0(ctx context.Context, marshaler runtime.Marshaler, server PresenceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq HeartbeatRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.Heartbeat(ctx, &protoReq) + return msg, metadata, err +} + +var filter_PresenceService_GetOnlineUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_PresenceService_GetOnlineUsers_0(ctx context.Context, marshaler runtime.Marshaler, client PresenceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetOnlineUsersRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_PresenceService_GetOnlineUsers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetOnlineUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_PresenceService_GetOnlineUsers_0(ctx context.Context, marshaler runtime.Marshaler, server PresenceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetOnlineUsersRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_PresenceService_GetOnlineUsers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetOnlineUsers(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterChatServiceHandlerServer registers the http handlers for service ChatService to "mux". +// UnaryRPC :call ChatServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterChatServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterChatServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ChatServiceServer) error { + mux.Handle(http.MethodPost, pattern_ChatService_CreateDirectConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/CreateDirectConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/direct")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_CreateDirectConversation_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_CreateDirectConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_CreateGroupConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/CreateGroupConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/group")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_CreateGroupConversation_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_CreateGroupConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_GetConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/GetConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_GetConversation_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_GetConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_ListConversations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/ListConversations", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_ListConversations_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_ListConversations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_ChatService_UpdateGroupConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/UpdateGroupConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_UpdateGroupConversation_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_UpdateGroupConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_AddParticipants_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/AddParticipants", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/participants")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_AddParticipants_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_AddParticipants_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ChatService_RemoveParticipant_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/RemoveParticipant", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/participants/{user_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_RemoveParticipant_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_RemoveParticipant_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_LeaveConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/LeaveConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/leave")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_LeaveConversation_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_LeaveConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_SendMessage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/SendMessage", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_SendMessage_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_SendMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_ChatService_EditMessage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/EditMessage", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_EditMessage_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_EditMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ChatService_DeleteMessage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/DeleteMessage", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_DeleteMessage_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_DeleteMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_ListMessages_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/ListMessages", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_ListMessages_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_ListMessages_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + mux.Handle(http.MethodGet, pattern_ChatService_StreamChatEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + mux.Handle(http.MethodPost, pattern_ChatService_MarkConversationRead_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/MarkConversationRead", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/read")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_MarkConversationRead_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_MarkConversationRead_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ChatService_ClearConversationHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/ClearConversationHistory", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/history")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_ClearConversationHistory_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_ClearConversationHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_SetTyping_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/SetTyping", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/typing")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_SetTyping_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_SetTyping_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_GetMessageEditHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/GetMessageEditHistory", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}/history")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_GetMessageEditHistory_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_GetMessageEditHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_UploadChatAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.ChatService/UploadChatAttachment", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ChatService_UploadChatAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_UploadChatAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterPresenceServiceHandlerServer registers the http handlers for service PresenceService to "mux". +// UnaryRPC :call PresenceServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterPresenceServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterPresenceServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server PresenceServiceServer) error { + mux.Handle(http.MethodPost, pattern_PresenceService_Heartbeat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.PresenceService/Heartbeat", runtime.WithHTTPPathPattern("/api/v1/iam/presence/heartbeat")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_PresenceService_Heartbeat_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PresenceService_Heartbeat_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_PresenceService_GetOnlineUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/iam.v1.PresenceService/GetOnlineUsers", runtime.WithHTTPPathPattern("/api/v1/iam/presence/online")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_PresenceService_GetOnlineUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PresenceService_GetOnlineUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterChatServiceHandlerFromEndpoint is same as RegisterChatServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterChatServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterChatServiceHandler(ctx, mux, conn) +} + +// RegisterChatServiceHandler registers the http handlers for service ChatService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterChatServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterChatServiceHandlerClient(ctx, mux, NewChatServiceClient(conn)) +} + +// RegisterChatServiceHandlerClient registers the http handlers for service ChatService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ChatServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ChatServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "ChatServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterChatServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ChatServiceClient) error { + mux.Handle(http.MethodPost, pattern_ChatService_CreateDirectConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/CreateDirectConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/direct")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_CreateDirectConversation_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_CreateDirectConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_CreateGroupConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/CreateGroupConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/group")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_CreateGroupConversation_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_CreateGroupConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_GetConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/GetConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_GetConversation_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_GetConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_ListConversations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/ListConversations", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_ListConversations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_ListConversations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_ChatService_UpdateGroupConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/UpdateGroupConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_UpdateGroupConversation_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_UpdateGroupConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_AddParticipants_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/AddParticipants", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/participants")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_AddParticipants_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_AddParticipants_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ChatService_RemoveParticipant_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/RemoveParticipant", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/participants/{user_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_RemoveParticipant_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_RemoveParticipant_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_LeaveConversation_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/LeaveConversation", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/leave")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_LeaveConversation_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_LeaveConversation_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_SendMessage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/SendMessage", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_SendMessage_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_SendMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_ChatService_EditMessage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/EditMessage", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_EditMessage_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_EditMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ChatService_DeleteMessage_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/DeleteMessage", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_DeleteMessage_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_DeleteMessage_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_ListMessages_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/ListMessages", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_ListMessages_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_ListMessages_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_StreamChatEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/StreamChatEvents", runtime.WithHTTPPathPattern("/api/v1/iam/chat/events")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_StreamChatEvents_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_StreamChatEvents_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_MarkConversationRead_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/MarkConversationRead", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/read")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_MarkConversationRead_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_MarkConversationRead_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ChatService_ClearConversationHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/ClearConversationHistory", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/history")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_ClearConversationHistory_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_ClearConversationHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_SetTyping_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/SetTyping", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/typing")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_SetTyping_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_SetTyping_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ChatService_GetMessageEditHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/GetMessageEditHistory", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/messages/{message_id}/history")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_GetMessageEditHistory_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_GetMessageEditHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ChatService_UploadChatAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.ChatService/UploadChatAttachment", runtime.WithHTTPPathPattern("/api/v1/iam/chat/conversations/{conversation_id}/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ChatService_UploadChatAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ChatService_UploadChatAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_ChatService_CreateDirectConversation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "chat", "conversations", "direct"}, "")) + pattern_ChatService_CreateGroupConversation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"api", "v1", "iam", "chat", "conversations", "group"}, "")) + pattern_ChatService_GetConversation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id"}, "")) + pattern_ChatService_ListConversations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "chat", "conversations"}, "")) + pattern_ChatService_UpdateGroupConversation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id"}, "")) + pattern_ChatService_AddParticipants_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "participants"}, "")) + pattern_ChatService_RemoveParticipant_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6, 1, 0, 4, 1, 5, 7}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "participants", "user_id"}, "")) + pattern_ChatService_LeaveConversation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "leave"}, "")) + pattern_ChatService_SendMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "messages"}, "")) + pattern_ChatService_EditMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6, 1, 0, 4, 1, 5, 7}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "messages", "message_id"}, "")) + pattern_ChatService_DeleteMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6, 1, 0, 4, 1, 5, 7}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "messages", "message_id"}, "")) + pattern_ChatService_ListMessages_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "messages"}, "")) + pattern_ChatService_StreamChatEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "chat", "events"}, "")) + pattern_ChatService_MarkConversationRead_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "read"}, "")) + pattern_ChatService_ClearConversationHistory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "history"}, "")) + pattern_ChatService_SetTyping_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "typing"}, "")) + pattern_ChatService_GetMessageEditHistory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6, 1, 0, 4, 1, 5, 7, 2, 8}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "messages", "message_id", "history"}, "")) + pattern_ChatService_UploadChatAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "iam", "chat", "conversations", "conversation_id", "attachments"}, "")) +) + +var ( + forward_ChatService_CreateDirectConversation_0 = runtime.ForwardResponseMessage + forward_ChatService_CreateGroupConversation_0 = runtime.ForwardResponseMessage + forward_ChatService_GetConversation_0 = runtime.ForwardResponseMessage + forward_ChatService_ListConversations_0 = runtime.ForwardResponseMessage + forward_ChatService_UpdateGroupConversation_0 = runtime.ForwardResponseMessage + forward_ChatService_AddParticipants_0 = runtime.ForwardResponseMessage + forward_ChatService_RemoveParticipant_0 = runtime.ForwardResponseMessage + forward_ChatService_LeaveConversation_0 = runtime.ForwardResponseMessage + forward_ChatService_SendMessage_0 = runtime.ForwardResponseMessage + forward_ChatService_EditMessage_0 = runtime.ForwardResponseMessage + forward_ChatService_DeleteMessage_0 = runtime.ForwardResponseMessage + forward_ChatService_ListMessages_0 = runtime.ForwardResponseMessage + forward_ChatService_StreamChatEvents_0 = runtime.ForwardResponseStream + forward_ChatService_MarkConversationRead_0 = runtime.ForwardResponseMessage + forward_ChatService_ClearConversationHistory_0 = runtime.ForwardResponseMessage + forward_ChatService_SetTyping_0 = runtime.ForwardResponseMessage + forward_ChatService_GetMessageEditHistory_0 = runtime.ForwardResponseMessage + forward_ChatService_UploadChatAttachment_0 = runtime.ForwardResponseMessage +) + +// RegisterPresenceServiceHandlerFromEndpoint is same as RegisterPresenceServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterPresenceServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterPresenceServiceHandler(ctx, mux, conn) +} + +// RegisterPresenceServiceHandler registers the http handlers for service PresenceService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterPresenceServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterPresenceServiceHandlerClient(ctx, mux, NewPresenceServiceClient(conn)) +} + +// RegisterPresenceServiceHandlerClient registers the http handlers for service PresenceService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "PresenceServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "PresenceServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "PresenceServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterPresenceServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client PresenceServiceClient) error { + mux.Handle(http.MethodPost, pattern_PresenceService_Heartbeat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.PresenceService/Heartbeat", runtime.WithHTTPPathPattern("/api/v1/iam/presence/heartbeat")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_PresenceService_Heartbeat_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PresenceService_Heartbeat_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_PresenceService_GetOnlineUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/iam.v1.PresenceService/GetOnlineUsers", runtime.WithHTTPPathPattern("/api/v1/iam/presence/online")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_PresenceService_GetOnlineUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PresenceService_GetOnlineUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_PresenceService_Heartbeat_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "presence", "heartbeat"}, "")) + pattern_PresenceService_GetOnlineUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "iam", "presence", "online"}, "")) +) + +var ( + forward_PresenceService_Heartbeat_0 = runtime.ForwardResponseMessage + forward_PresenceService_GetOnlineUsers_0 = runtime.ForwardResponseMessage +) diff --git a/gen/iam/v1/chat_grpc.pb.go b/gen/iam/v1/chat_grpc.pb.go new file mode 100644 index 00000000..311626ec --- /dev/null +++ b/gen/iam/v1/chat_grpc.pb.go @@ -0,0 +1,963 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: iam/v1/chat.proto + +package iamv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ChatService_CreateDirectConversation_FullMethodName = "/iam.v1.ChatService/CreateDirectConversation" + ChatService_CreateGroupConversation_FullMethodName = "/iam.v1.ChatService/CreateGroupConversation" + ChatService_GetConversation_FullMethodName = "/iam.v1.ChatService/GetConversation" + ChatService_ListConversations_FullMethodName = "/iam.v1.ChatService/ListConversations" + ChatService_UpdateGroupConversation_FullMethodName = "/iam.v1.ChatService/UpdateGroupConversation" + ChatService_AddParticipants_FullMethodName = "/iam.v1.ChatService/AddParticipants" + ChatService_RemoveParticipant_FullMethodName = "/iam.v1.ChatService/RemoveParticipant" + ChatService_LeaveConversation_FullMethodName = "/iam.v1.ChatService/LeaveConversation" + ChatService_SendMessage_FullMethodName = "/iam.v1.ChatService/SendMessage" + ChatService_EditMessage_FullMethodName = "/iam.v1.ChatService/EditMessage" + ChatService_DeleteMessage_FullMethodName = "/iam.v1.ChatService/DeleteMessage" + ChatService_ListMessages_FullMethodName = "/iam.v1.ChatService/ListMessages" + ChatService_StreamChatEvents_FullMethodName = "/iam.v1.ChatService/StreamChatEvents" + ChatService_MarkConversationRead_FullMethodName = "/iam.v1.ChatService/MarkConversationRead" + ChatService_ClearConversationHistory_FullMethodName = "/iam.v1.ChatService/ClearConversationHistory" + ChatService_SetTyping_FullMethodName = "/iam.v1.ChatService/SetTyping" + ChatService_GetMessageEditHistory_FullMethodName = "/iam.v1.ChatService/GetMessageEditHistory" + ChatService_UploadChatAttachment_FullMethodName = "/iam.v1.ChatService/UploadChatAttachment" +) + +// ChatServiceClient is the client API for ChatService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// ChatService manages real-time chat conversations and messaging. +type ChatServiceClient interface { + // CreateDirectConversation creates a 1:1 direct message conversation with another user. + CreateDirectConversation(ctx context.Context, in *CreateDirectConversationRequest, opts ...grpc.CallOption) (*CreateDirectConversationResponse, error) + // CreateGroupConversation creates a group conversation with multiple participants. + CreateGroupConversation(ctx context.Context, in *CreateGroupConversationRequest, opts ...grpc.CallOption) (*CreateGroupConversationResponse, error) + // GetConversation returns a single conversation by ID. + GetConversation(ctx context.Context, in *GetConversationRequest, opts ...grpc.CallOption) (*GetConversationResponse, error) + // ListConversations returns paginated conversations for the current user. + ListConversations(ctx context.Context, in *ListConversationsRequest, opts ...grpc.CallOption) (*ListConversationsResponse, error) + // UpdateGroupConversation updates a group conversation name or avatar. + UpdateGroupConversation(ctx context.Context, in *UpdateGroupConversationRequest, opts ...grpc.CallOption) (*UpdateGroupConversationResponse, error) + // AddParticipants adds users to a group conversation. + AddParticipants(ctx context.Context, in *AddParticipantsRequest, opts ...grpc.CallOption) (*AddParticipantsResponse, error) + // RemoveParticipant removes a user from a group conversation. + RemoveParticipant(ctx context.Context, in *RemoveParticipantRequest, opts ...grpc.CallOption) (*RemoveParticipantResponse, error) + // LeaveConversation allows the current user to leave a group conversation. + LeaveConversation(ctx context.Context, in *LeaveConversationRequest, opts ...grpc.CallOption) (*LeaveConversationResponse, error) + // SendMessage sends a new message to a conversation. + SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*SendMessageResponse, error) + // EditMessage edits an existing message body. + EditMessage(ctx context.Context, in *EditMessageRequest, opts ...grpc.CallOption) (*EditMessageResponse, error) + // DeleteMessage soft-deletes a message in a conversation. + DeleteMessage(ctx context.Context, in *DeleteMessageRequest, opts ...grpc.CallOption) (*DeleteMessageResponse, error) + // ListMessages returns paginated messages for a conversation using cursor-based pagination. + ListMessages(ctx context.Context, in *ListMessagesRequest, opts ...grpc.CallOption) (*ListMessagesResponse, error) + // StreamChatEvents opens a server-streaming connection for real-time chat events. + StreamChatEvents(ctx context.Context, in *StreamChatEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamChatEventsResponse], error) + // MarkConversationRead marks all messages in a conversation as read by the current user. + MarkConversationRead(ctx context.Context, in *MarkConversationReadRequest, opts ...grpc.CallOption) (*MarkConversationReadResponse, error) + // ClearConversationHistory clears the calling user's own view of a conversation's + // message history. Messages remain visible to other participants. + ClearConversationHistory(ctx context.Context, in *ClearConversationHistoryRequest, opts ...grpc.CallOption) (*ClearConversationHistoryResponse, error) + // SetTyping sets the typing indicator for the current user in a conversation. + SetTyping(ctx context.Context, in *SetTypingRequest, opts ...grpc.CallOption) (*SetTypingResponse, error) + // GetMessageEditHistory returns the edit history for a specific message. + GetMessageEditHistory(ctx context.Context, in *GetMessageEditHistoryRequest, opts ...grpc.CallOption) (*GetMessageEditHistoryResponse, error) + // UploadChatAttachment uploads a file to a conversation and returns its + // attachment metadata. The returned attachment_id is passed to SendMessage. + UploadChatAttachment(ctx context.Context, in *UploadChatAttachmentRequest, opts ...grpc.CallOption) (*UploadChatAttachmentResponse, error) +} + +type chatServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewChatServiceClient(cc grpc.ClientConnInterface) ChatServiceClient { + return &chatServiceClient{cc} +} + +func (c *chatServiceClient) CreateDirectConversation(ctx context.Context, in *CreateDirectConversationRequest, opts ...grpc.CallOption) (*CreateDirectConversationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateDirectConversationResponse) + err := c.cc.Invoke(ctx, ChatService_CreateDirectConversation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) CreateGroupConversation(ctx context.Context, in *CreateGroupConversationRequest, opts ...grpc.CallOption) (*CreateGroupConversationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateGroupConversationResponse) + err := c.cc.Invoke(ctx, ChatService_CreateGroupConversation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) GetConversation(ctx context.Context, in *GetConversationRequest, opts ...grpc.CallOption) (*GetConversationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetConversationResponse) + err := c.cc.Invoke(ctx, ChatService_GetConversation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) ListConversations(ctx context.Context, in *ListConversationsRequest, opts ...grpc.CallOption) (*ListConversationsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListConversationsResponse) + err := c.cc.Invoke(ctx, ChatService_ListConversations_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) UpdateGroupConversation(ctx context.Context, in *UpdateGroupConversationRequest, opts ...grpc.CallOption) (*UpdateGroupConversationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateGroupConversationResponse) + err := c.cc.Invoke(ctx, ChatService_UpdateGroupConversation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) AddParticipants(ctx context.Context, in *AddParticipantsRequest, opts ...grpc.CallOption) (*AddParticipantsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddParticipantsResponse) + err := c.cc.Invoke(ctx, ChatService_AddParticipants_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) RemoveParticipant(ctx context.Context, in *RemoveParticipantRequest, opts ...grpc.CallOption) (*RemoveParticipantResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveParticipantResponse) + err := c.cc.Invoke(ctx, ChatService_RemoveParticipant_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) LeaveConversation(ctx context.Context, in *LeaveConversationRequest, opts ...grpc.CallOption) (*LeaveConversationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LeaveConversationResponse) + err := c.cc.Invoke(ctx, ChatService_LeaveConversation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) SendMessage(ctx context.Context, in *SendMessageRequest, opts ...grpc.CallOption) (*SendMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendMessageResponse) + err := c.cc.Invoke(ctx, ChatService_SendMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) EditMessage(ctx context.Context, in *EditMessageRequest, opts ...grpc.CallOption) (*EditMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EditMessageResponse) + err := c.cc.Invoke(ctx, ChatService_EditMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) DeleteMessage(ctx context.Context, in *DeleteMessageRequest, opts ...grpc.CallOption) (*DeleteMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteMessageResponse) + err := c.cc.Invoke(ctx, ChatService_DeleteMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) ListMessages(ctx context.Context, in *ListMessagesRequest, opts ...grpc.CallOption) (*ListMessagesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMessagesResponse) + err := c.cc.Invoke(ctx, ChatService_ListMessages_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) StreamChatEvents(ctx context.Context, in *StreamChatEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamChatEventsResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[0], ChatService_StreamChatEvents_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[StreamChatEventsRequest, StreamChatEventsResponse]{ClientStream: 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 +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ChatService_StreamChatEventsClient = grpc.ServerStreamingClient[StreamChatEventsResponse] + +func (c *chatServiceClient) MarkConversationRead(ctx context.Context, in *MarkConversationReadRequest, opts ...grpc.CallOption) (*MarkConversationReadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MarkConversationReadResponse) + err := c.cc.Invoke(ctx, ChatService_MarkConversationRead_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) ClearConversationHistory(ctx context.Context, in *ClearConversationHistoryRequest, opts ...grpc.CallOption) (*ClearConversationHistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClearConversationHistoryResponse) + err := c.cc.Invoke(ctx, ChatService_ClearConversationHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) SetTyping(ctx context.Context, in *SetTypingRequest, opts ...grpc.CallOption) (*SetTypingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SetTypingResponse) + err := c.cc.Invoke(ctx, ChatService_SetTyping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) GetMessageEditHistory(ctx context.Context, in *GetMessageEditHistoryRequest, opts ...grpc.CallOption) (*GetMessageEditHistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetMessageEditHistoryResponse) + err := c.cc.Invoke(ctx, ChatService_GetMessageEditHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) UploadChatAttachment(ctx context.Context, in *UploadChatAttachmentRequest, opts ...grpc.CallOption) (*UploadChatAttachmentResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UploadChatAttachmentResponse) + err := c.cc.Invoke(ctx, ChatService_UploadChatAttachment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ChatServiceServer is the server API for ChatService service. +// All implementations must embed UnimplementedChatServiceServer +// for forward compatibility. +// +// ChatService manages real-time chat conversations and messaging. +type ChatServiceServer interface { + // CreateDirectConversation creates a 1:1 direct message conversation with another user. + CreateDirectConversation(context.Context, *CreateDirectConversationRequest) (*CreateDirectConversationResponse, error) + // CreateGroupConversation creates a group conversation with multiple participants. + CreateGroupConversation(context.Context, *CreateGroupConversationRequest) (*CreateGroupConversationResponse, error) + // GetConversation returns a single conversation by ID. + GetConversation(context.Context, *GetConversationRequest) (*GetConversationResponse, error) + // ListConversations returns paginated conversations for the current user. + ListConversations(context.Context, *ListConversationsRequest) (*ListConversationsResponse, error) + // UpdateGroupConversation updates a group conversation name or avatar. + UpdateGroupConversation(context.Context, *UpdateGroupConversationRequest) (*UpdateGroupConversationResponse, error) + // AddParticipants adds users to a group conversation. + AddParticipants(context.Context, *AddParticipantsRequest) (*AddParticipantsResponse, error) + // RemoveParticipant removes a user from a group conversation. + RemoveParticipant(context.Context, *RemoveParticipantRequest) (*RemoveParticipantResponse, error) + // LeaveConversation allows the current user to leave a group conversation. + LeaveConversation(context.Context, *LeaveConversationRequest) (*LeaveConversationResponse, error) + // SendMessage sends a new message to a conversation. + SendMessage(context.Context, *SendMessageRequest) (*SendMessageResponse, error) + // EditMessage edits an existing message body. + EditMessage(context.Context, *EditMessageRequest) (*EditMessageResponse, error) + // DeleteMessage soft-deletes a message in a conversation. + DeleteMessage(context.Context, *DeleteMessageRequest) (*DeleteMessageResponse, error) + // ListMessages returns paginated messages for a conversation using cursor-based pagination. + ListMessages(context.Context, *ListMessagesRequest) (*ListMessagesResponse, error) + // StreamChatEvents opens a server-streaming connection for real-time chat events. + StreamChatEvents(*StreamChatEventsRequest, grpc.ServerStreamingServer[StreamChatEventsResponse]) error + // MarkConversationRead marks all messages in a conversation as read by the current user. + MarkConversationRead(context.Context, *MarkConversationReadRequest) (*MarkConversationReadResponse, error) + // ClearConversationHistory clears the calling user's own view of a conversation's + // message history. Messages remain visible to other participants. + ClearConversationHistory(context.Context, *ClearConversationHistoryRequest) (*ClearConversationHistoryResponse, error) + // SetTyping sets the typing indicator for the current user in a conversation. + SetTyping(context.Context, *SetTypingRequest) (*SetTypingResponse, error) + // GetMessageEditHistory returns the edit history for a specific message. + GetMessageEditHistory(context.Context, *GetMessageEditHistoryRequest) (*GetMessageEditHistoryResponse, error) + // UploadChatAttachment uploads a file to a conversation and returns its + // attachment metadata. The returned attachment_id is passed to SendMessage. + UploadChatAttachment(context.Context, *UploadChatAttachmentRequest) (*UploadChatAttachmentResponse, error) + mustEmbedUnimplementedChatServiceServer() +} + +// UnimplementedChatServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedChatServiceServer struct{} + +func (UnimplementedChatServiceServer) CreateDirectConversation(context.Context, *CreateDirectConversationRequest) (*CreateDirectConversationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateDirectConversation not implemented") +} +func (UnimplementedChatServiceServer) CreateGroupConversation(context.Context, *CreateGroupConversationRequest) (*CreateGroupConversationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateGroupConversation not implemented") +} +func (UnimplementedChatServiceServer) GetConversation(context.Context, *GetConversationRequest) (*GetConversationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetConversation not implemented") +} +func (UnimplementedChatServiceServer) ListConversations(context.Context, *ListConversationsRequest) (*ListConversationsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListConversations not implemented") +} +func (UnimplementedChatServiceServer) UpdateGroupConversation(context.Context, *UpdateGroupConversationRequest) (*UpdateGroupConversationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateGroupConversation not implemented") +} +func (UnimplementedChatServiceServer) AddParticipants(context.Context, *AddParticipantsRequest) (*AddParticipantsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddParticipants not implemented") +} +func (UnimplementedChatServiceServer) RemoveParticipant(context.Context, *RemoveParticipantRequest) (*RemoveParticipantResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveParticipant not implemented") +} +func (UnimplementedChatServiceServer) LeaveConversation(context.Context, *LeaveConversationRequest) (*LeaveConversationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method LeaveConversation not implemented") +} +func (UnimplementedChatServiceServer) SendMessage(context.Context, *SendMessageRequest) (*SendMessageResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SendMessage not implemented") +} +func (UnimplementedChatServiceServer) EditMessage(context.Context, *EditMessageRequest) (*EditMessageResponse, error) { + return nil, status.Error(codes.Unimplemented, "method EditMessage not implemented") +} +func (UnimplementedChatServiceServer) DeleteMessage(context.Context, *DeleteMessageRequest) (*DeleteMessageResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteMessage not implemented") +} +func (UnimplementedChatServiceServer) ListMessages(context.Context, *ListMessagesRequest) (*ListMessagesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListMessages not implemented") +} +func (UnimplementedChatServiceServer) StreamChatEvents(*StreamChatEventsRequest, grpc.ServerStreamingServer[StreamChatEventsResponse]) error { + return status.Error(codes.Unimplemented, "method StreamChatEvents not implemented") +} +func (UnimplementedChatServiceServer) MarkConversationRead(context.Context, *MarkConversationReadRequest) (*MarkConversationReadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method MarkConversationRead not implemented") +} +func (UnimplementedChatServiceServer) ClearConversationHistory(context.Context, *ClearConversationHistoryRequest) (*ClearConversationHistoryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ClearConversationHistory not implemented") +} +func (UnimplementedChatServiceServer) SetTyping(context.Context, *SetTypingRequest) (*SetTypingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SetTyping not implemented") +} +func (UnimplementedChatServiceServer) GetMessageEditHistory(context.Context, *GetMessageEditHistoryRequest) (*GetMessageEditHistoryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetMessageEditHistory not implemented") +} +func (UnimplementedChatServiceServer) UploadChatAttachment(context.Context, *UploadChatAttachmentRequest) (*UploadChatAttachmentResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UploadChatAttachment not implemented") +} +func (UnimplementedChatServiceServer) mustEmbedUnimplementedChatServiceServer() {} +func (UnimplementedChatServiceServer) testEmbeddedByValue() {} + +// UnsafeChatServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ChatServiceServer will +// result in compilation errors. +type UnsafeChatServiceServer interface { + mustEmbedUnimplementedChatServiceServer() +} + +func RegisterChatServiceServer(s grpc.ServiceRegistrar, srv ChatServiceServer) { + // If the following call panics, it indicates UnimplementedChatServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ChatService_ServiceDesc, srv) +} + +func _ChatService_CreateDirectConversation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateDirectConversationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).CreateDirectConversation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_CreateDirectConversation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).CreateDirectConversation(ctx, req.(*CreateDirectConversationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_CreateGroupConversation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateGroupConversationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).CreateGroupConversation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_CreateGroupConversation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).CreateGroupConversation(ctx, req.(*CreateGroupConversationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_GetConversation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetConversationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).GetConversation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_GetConversation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).GetConversation(ctx, req.(*GetConversationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_ListConversations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListConversationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).ListConversations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_ListConversations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).ListConversations(ctx, req.(*ListConversationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_UpdateGroupConversation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateGroupConversationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).UpdateGroupConversation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_UpdateGroupConversation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).UpdateGroupConversation(ctx, req.(*UpdateGroupConversationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_AddParticipants_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddParticipantsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).AddParticipants(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_AddParticipants_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).AddParticipants(ctx, req.(*AddParticipantsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_RemoveParticipant_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveParticipantRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).RemoveParticipant(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_RemoveParticipant_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).RemoveParticipant(ctx, req.(*RemoveParticipantRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_LeaveConversation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LeaveConversationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).LeaveConversation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_LeaveConversation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).LeaveConversation(ctx, req.(*LeaveConversationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).SendMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_SendMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).SendMessage(ctx, req.(*SendMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_EditMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EditMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).EditMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_EditMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).EditMessage(ctx, req.(*EditMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_DeleteMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).DeleteMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_DeleteMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).DeleteMessage(ctx, req.(*DeleteMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_ListMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).ListMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_ListMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).ListMessages(ctx, req.(*ListMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_StreamChatEvents_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(StreamChatEventsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ChatServiceServer).StreamChatEvents(m, &grpc.GenericServerStream[StreamChatEventsRequest, StreamChatEventsResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ChatService_StreamChatEventsServer = grpc.ServerStreamingServer[StreamChatEventsResponse] + +func _ChatService_MarkConversationRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MarkConversationReadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).MarkConversationRead(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_MarkConversationRead_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).MarkConversationRead(ctx, req.(*MarkConversationReadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_ClearConversationHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClearConversationHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).ClearConversationHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_ClearConversationHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).ClearConversationHistory(ctx, req.(*ClearConversationHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_SetTyping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetTypingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).SetTyping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_SetTyping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).SetTyping(ctx, req.(*SetTypingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_GetMessageEditHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMessageEditHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).GetMessageEditHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_GetMessageEditHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).GetMessageEditHistory(ctx, req.(*GetMessageEditHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_UploadChatAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UploadChatAttachmentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).UploadChatAttachment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_UploadChatAttachment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).UploadChatAttachment(ctx, req.(*UploadChatAttachmentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ChatService_ServiceDesc is the grpc.ServiceDesc for ChatService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ChatService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "iam.v1.ChatService", + HandlerType: (*ChatServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateDirectConversation", + Handler: _ChatService_CreateDirectConversation_Handler, + }, + { + MethodName: "CreateGroupConversation", + Handler: _ChatService_CreateGroupConversation_Handler, + }, + { + MethodName: "GetConversation", + Handler: _ChatService_GetConversation_Handler, + }, + { + MethodName: "ListConversations", + Handler: _ChatService_ListConversations_Handler, + }, + { + MethodName: "UpdateGroupConversation", + Handler: _ChatService_UpdateGroupConversation_Handler, + }, + { + MethodName: "AddParticipants", + Handler: _ChatService_AddParticipants_Handler, + }, + { + MethodName: "RemoveParticipant", + Handler: _ChatService_RemoveParticipant_Handler, + }, + { + MethodName: "LeaveConversation", + Handler: _ChatService_LeaveConversation_Handler, + }, + { + MethodName: "SendMessage", + Handler: _ChatService_SendMessage_Handler, + }, + { + MethodName: "EditMessage", + Handler: _ChatService_EditMessage_Handler, + }, + { + MethodName: "DeleteMessage", + Handler: _ChatService_DeleteMessage_Handler, + }, + { + MethodName: "ListMessages", + Handler: _ChatService_ListMessages_Handler, + }, + { + MethodName: "MarkConversationRead", + Handler: _ChatService_MarkConversationRead_Handler, + }, + { + MethodName: "ClearConversationHistory", + Handler: _ChatService_ClearConversationHistory_Handler, + }, + { + MethodName: "SetTyping", + Handler: _ChatService_SetTyping_Handler, + }, + { + MethodName: "GetMessageEditHistory", + Handler: _ChatService_GetMessageEditHistory_Handler, + }, + { + MethodName: "UploadChatAttachment", + Handler: _ChatService_UploadChatAttachment_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamChatEvents", + Handler: _ChatService_StreamChatEvents_Handler, + ServerStreams: true, + }, + }, + Metadata: "iam/v1/chat.proto", +} + +const ( + PresenceService_Heartbeat_FullMethodName = "/iam.v1.PresenceService/Heartbeat" + PresenceService_GetOnlineUsers_FullMethodName = "/iam.v1.PresenceService/GetOnlineUsers" +) + +// PresenceServiceClient is the client API for PresenceService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// PresenceService tracks user online status via heartbeat and exposes online user lists. +type PresenceServiceClient interface { + // Heartbeat updates the current user online presence with a TTL-based expiry. + Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) + // GetOnlineUsers returns the list of currently online user IDs. + GetOnlineUsers(ctx context.Context, in *GetOnlineUsersRequest, opts ...grpc.CallOption) (*GetOnlineUsersResponse, error) +} + +type presenceServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPresenceServiceClient(cc grpc.ClientConnInterface) PresenceServiceClient { + return &presenceServiceClient{cc} +} + +func (c *presenceServiceClient) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HeartbeatResponse) + err := c.cc.Invoke(ctx, PresenceService_Heartbeat_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *presenceServiceClient) GetOnlineUsers(ctx context.Context, in *GetOnlineUsersRequest, opts ...grpc.CallOption) (*GetOnlineUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetOnlineUsersResponse) + err := c.cc.Invoke(ctx, PresenceService_GetOnlineUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PresenceServiceServer is the server API for PresenceService service. +// All implementations must embed UnimplementedPresenceServiceServer +// for forward compatibility. +// +// PresenceService tracks user online status via heartbeat and exposes online user lists. +type PresenceServiceServer interface { + // Heartbeat updates the current user online presence with a TTL-based expiry. + Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) + // GetOnlineUsers returns the list of currently online user IDs. + GetOnlineUsers(context.Context, *GetOnlineUsersRequest) (*GetOnlineUsersResponse, error) + mustEmbedUnimplementedPresenceServiceServer() +} + +// UnimplementedPresenceServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPresenceServiceServer struct{} + +func (UnimplementedPresenceServiceServer) Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Heartbeat not implemented") +} +func (UnimplementedPresenceServiceServer) GetOnlineUsers(context.Context, *GetOnlineUsersRequest) (*GetOnlineUsersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetOnlineUsers not implemented") +} +func (UnimplementedPresenceServiceServer) mustEmbedUnimplementedPresenceServiceServer() {} +func (UnimplementedPresenceServiceServer) testEmbeddedByValue() {} + +// UnsafePresenceServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PresenceServiceServer will +// result in compilation errors. +type UnsafePresenceServiceServer interface { + mustEmbedUnimplementedPresenceServiceServer() +} + +func RegisterPresenceServiceServer(s grpc.ServiceRegistrar, srv PresenceServiceServer) { + // If the following call panics, it indicates UnimplementedPresenceServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&PresenceService_ServiceDesc, srv) +} + +func _PresenceService_Heartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HeartbeatRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PresenceServiceServer).Heartbeat(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PresenceService_Heartbeat_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PresenceServiceServer).Heartbeat(ctx, req.(*HeartbeatRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PresenceService_GetOnlineUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOnlineUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PresenceServiceServer).GetOnlineUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PresenceService_GetOnlineUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PresenceServiceServer).GetOnlineUsers(ctx, req.(*GetOnlineUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// PresenceService_ServiceDesc is the grpc.ServiceDesc for PresenceService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PresenceService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "iam.v1.PresenceService", + HandlerType: (*PresenceServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Heartbeat", + Handler: _PresenceService_Heartbeat_Handler, + }, + { + MethodName: "GetOnlineUsers", + Handler: _PresenceService_GetOnlineUsers_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "iam/v1/chat.proto", +} diff --git a/gen/openapi/iam/v1/chat.swagger.json b/gen/openapi/iam/v1/chat.swagger.json new file mode 100644 index 00000000..a5296706 --- /dev/null +++ b/gen/openapi/iam/v1/chat.swagger.json @@ -0,0 +1,1485 @@ +{ + "swagger": "2.0", + "info": { + "title": "iam/v1/chat.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "ChatService" + }, + { + "name": "PresenceService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/iam/chat/conversations": { + "get": { + "summary": "ListConversations returns paginated conversations for the current user.", + "operationId": "ChatService_ListConversations", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListConversationsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "search", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/direct": { + "post": { + "summary": "CreateDirectConversation creates a 1:1 direct message conversation with another user.", + "operationId": "ChatService_CreateDirectConversation", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CreateDirectConversationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "CreateDirectConversationRequest creates a 1:1 direct conversation.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1CreateDirectConversationRequest" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/group": { + "post": { + "summary": "CreateGroupConversation creates a group conversation with multiple participants.", + "operationId": "ChatService_CreateGroupConversation", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1CreateGroupConversationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "CreateGroupConversationRequest creates a group conversation.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1CreateGroupConversationRequest" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}": { + "get": { + "summary": "GetConversation returns a single conversation by ID.", + "operationId": "ChatService_GetConversation", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetConversationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + }, + "put": { + "summary": "UpdateGroupConversation updates a group conversation name or avatar.", + "operationId": "ChatService_UpdateGroupConversation", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1UpdateGroupConversationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceUpdateGroupConversationBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/attachments": { + "post": { + "summary": "UploadChatAttachment uploads a file to a conversation and returns its\nattachment metadata. The returned attachment_id is passed to SendMessage.", + "operationId": "ChatService_UploadChatAttachment", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1UploadChatAttachmentResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceUploadChatAttachmentBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/history": { + "delete": { + "summary": "ClearConversationHistory clears the calling user's own view of a conversation's\nmessage history. Messages remain visible to other participants.", + "operationId": "ChatService_ClearConversationHistory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ClearConversationHistoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/leave": { + "post": { + "summary": "LeaveConversation allows the current user to leave a group conversation.", + "operationId": "ChatService_LeaveConversation", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1LeaveConversationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceLeaveConversationBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/messages": { + "get": { + "summary": "ListMessages returns paginated messages for a conversation using cursor-based pagination.", + "operationId": "ChatService_ListMessages", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListMessagesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "beforeCursor", + "description": "Opaque base64 cursor; empty = start from latest messages.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + }, + "post": { + "summary": "SendMessage sends a new message to a conversation.", + "operationId": "ChatService_SendMessage", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1SendMessageResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceSendMessageBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/messages/{messageId}": { + "delete": { + "summary": "DeleteMessage soft-deletes a message in a conversation.", + "operationId": "ChatService_DeleteMessage", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DeleteMessageResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "messageId", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + }, + "put": { + "summary": "EditMessage edits an existing message body.", + "operationId": "ChatService_EditMessage", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1EditMessageResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "messageId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceEditMessageBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/messages/{messageId}/history": { + "get": { + "summary": "GetMessageEditHistory returns the edit history for a specific message.", + "operationId": "ChatService_GetMessageEditHistory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetMessageEditHistoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "messageId", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/participants": { + "post": { + "summary": "AddParticipants adds users to a group conversation.", + "operationId": "ChatService_AddParticipants", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1AddParticipantsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceAddParticipantsBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/participants/{userId}": { + "delete": { + "summary": "RemoveParticipant removes a user from a group conversation.", + "operationId": "ChatService_RemoveParticipant", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1RemoveParticipantResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "userId", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/read": { + "post": { + "summary": "MarkConversationRead marks all messages in a conversation as read by the current user.", + "operationId": "ChatService_MarkConversationRead", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1MarkConversationReadResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceMarkConversationReadBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/conversations/{conversationId}/typing": { + "post": { + "summary": "SetTyping sets the typing indicator for the current user in a conversation.", + "operationId": "ChatService_SetTyping", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1SetTypingResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ChatServiceSetTypingBody" + } + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/chat/events": { + "get": { + "summary": "StreamChatEvents opens a server-streaming connection for real-time chat events.", + "operationId": "ChatService_StreamChatEvents", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/v1StreamChatEventsResponse" + }, + "error": { + "$ref": "#/definitions/rpcStatus" + } + }, + "title": "Stream result of v1StreamChatEventsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "lastEventId", + "description": "Last event ID seen by the client; used to resume the stream after reconnect.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ChatService" + ] + } + }, + "/api/v1/iam/presence/heartbeat": { + "post": { + "summary": "Heartbeat updates the current user online presence with a TTL-based expiry.", + "operationId": "PresenceService_Heartbeat", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1HeartbeatResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "HeartbeatRequest sends a heartbeat to update online presence.", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1HeartbeatRequest" + } + } + ], + "tags": [ + "PresenceService" + ] + } + }, + "/api/v1/iam/presence/online": { + "get": { + "summary": "GetOnlineUsers returns the list of currently online user IDs.", + "operationId": "PresenceService_GetOnlineUsers", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1GetOnlineUsersResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "userIds", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "PresenceService" + ] + } + } + }, + "definitions": { + "ChatServiceAddParticipantsBody": { + "type": "object", + "properties": { + "userIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "description": "AddParticipantsRequest adds users to a group conversation." + }, + "ChatServiceEditMessageBody": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + }, + "description": "EditMessageRequest edits an existing message." + }, + "ChatServiceLeaveConversationBody": { + "type": "object", + "description": "LeaveConversationRequest allows the current user to leave a group conversation." + }, + "ChatServiceMarkConversationReadBody": { + "type": "object", + "description": "MarkConversationReadRequest marks a conversation as read." + }, + "ChatServiceSendMessageBody": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "replyToId": { + "type": "string", + "description": "ID of the message being replied to (optional, empty = no reply)." + }, + "attachmentIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Pre-uploaded attachment IDs to associate with this message (optional)." + } + }, + "description": "SendMessageRequest sends a message to a conversation." + }, + "ChatServiceSetTypingBody": { + "type": "object", + "properties": { + "isTyping": { + "type": "boolean" + } + }, + "description": "SetTypingRequest sets the typing indicator for the current user." + }, + "ChatServiceUpdateGroupConversationBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + } + }, + "description": "UpdateGroupConversationRequest updates a group conversation." + }, + "ChatServiceUploadChatAttachmentBody": { + "type": "object", + "properties": { + "fileName": { + "type": "string" + }, + "contentType": { + "type": "string" + }, + "fileData": { + "type": "string", + "format": "byte", + "description": "Raw file bytes (max 25MB)." + } + }, + "description": "UploadChatAttachmentRequest uploads a single file to a conversation." + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1AddParticipantsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "AddParticipantsResponse confirms participants were added." + }, + "v1AttachmentProto": { + "type": "object", + "properties": { + "attachmentId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "fileUrl": { + "type": "string" + }, + "contentType": { + "type": "string" + }, + "fileSize": { + "type": "string", + "format": "int64" + }, + "thumbnailUrl": { + "type": "string" + } + }, + "description": "AttachmentProto represents a file or image attached to a chat message." + }, + "v1BaseResponse": { + "type": "object", + "properties": { + "validationErrors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ValidationError" + }, + "description": "List of validation errors if any." + }, + "statusCode": { + "type": "string", + "description": "HTTP-like status code (e.g., \"200\", \"400\", \"404\", \"500\")." + }, + "isSuccess": { + "type": "boolean", + "description": "Indicates if the operation was successful." + }, + "message": { + "type": "string", + "description": "Human-readable message describing the result." + } + }, + "description": "BaseResponse is the standard response wrapper for all API responses." + }, + "v1ClearConversationHistoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "ClearConversationHistoryResponse confirms the caller's history was cleared." + }, + "v1ConversationProto": { + "type": "object", + "properties": { + "conversationId": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "participants": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ParticipantProto" + } + }, + "lastMessage": { + "$ref": "#/definitions/v1MessageProto" + }, + "unreadCount": { + "type": "integer", + "format": "int32" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "description": "ConversationProto represents a chat conversation (direct or group)." + }, + "v1CreateDirectConversationRequest": { + "type": "object", + "properties": { + "peerUserId": { + "type": "string" + } + }, + "description": "CreateDirectConversationRequest creates a 1:1 direct conversation." + }, + "v1CreateDirectConversationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1ConversationProto" + } + }, + "description": "CreateDirectConversationResponse returns the created direct conversation." + }, + "v1CreateGroupConversationRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "participantIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "description": "CreateGroupConversationRequest creates a group conversation." + }, + "v1CreateGroupConversationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1ConversationProto" + } + }, + "description": "CreateGroupConversationResponse returns the created group conversation." + }, + "v1DeleteEvent": { + "type": "object", + "properties": { + "conversationId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + }, + "description": "DeleteEvent represents a message deletion event." + }, + "v1DeleteMessageResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "DeleteMessageResponse confirms a message was deleted." + }, + "v1EditHistoryEntryProto": { + "type": "object", + "properties": { + "historyId": { + "type": "string", + "format": "int64" + }, + "body": { + "type": "string" + }, + "editedBy": { + "type": "string" + }, + "editedAt": { + "type": "string" + } + }, + "description": "EditHistoryEntryProto represents a single edit history entry for a message." + }, + "v1EditMessageResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1MessageProto" + } + }, + "description": "EditMessageResponse returns the edited message." + }, + "v1GetConversationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1ConversationProto" + } + }, + "description": "GetConversationResponse returns a single conversation." + }, + "v1GetMessageEditHistoryResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1EditHistoryEntryProto" + } + } + }, + "description": "GetMessageEditHistoryResponse returns the edit history for a message." + }, + "v1GetOnlineUsersResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "userIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "description": "GetOnlineUsersResponse returns the list of online user IDs." + }, + "v1HeartbeatRequest": { + "type": "object", + "description": "HeartbeatRequest sends a heartbeat to update online presence." + }, + "v1HeartbeatResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "HeartbeatResponse confirms the heartbeat was received." + }, + "v1LeaveConversationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "LeaveConversationResponse confirms the user left the conversation." + }, + "v1ListConversationsResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ConversationProto" + } + }, + "pagination": { + "$ref": "#/definitions/v1PaginationResponse" + } + }, + "description": "ListConversationsResponse returns a paginated list of conversations." + }, + "v1ListMessagesResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1MessageProto" + } + }, + "nextCursor": { + "type": "string" + }, + "hasMore": { + "type": "boolean" + } + }, + "description": "ListMessagesResponse returns a cursor-paginated list of messages." + }, + "v1MarkConversationReadResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "MarkConversationReadResponse confirms the conversation was marked as read." + }, + "v1MessageEvent": { + "type": "object", + "properties": { + "conversationId": { + "type": "string" + }, + "message": { + "$ref": "#/definitions/v1MessageProto" + } + }, + "description": "MessageEvent represents a new or edited message event." + }, + "v1MessageProto": { + "type": "object", + "properties": { + "messageId": { + "type": "string" + }, + "conversationId": { + "type": "string" + }, + "senderUserId": { + "type": "string" + }, + "senderName": { + "type": "string" + }, + "body": { + "type": "string" + }, + "isEdited": { + "type": "boolean" + }, + "isDeleted": { + "type": "boolean" + }, + "replyToId": { + "type": "string" + }, + "readReceipts": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ReadReceiptProto" + } + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1AttachmentProto" + } + } + }, + "description": "MessageProto represents a single chat message with decrypted body." + }, + "v1PaginationResponse": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "format": "int32", + "description": "Current page number." + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Number of items per page." + }, + "totalItems": { + "type": "string", + "format": "int64", + "description": "Total number of items across all pages." + }, + "totalPages": { + "type": "integer", + "format": "int32", + "description": "Total number of pages." + } + }, + "description": "PaginationResponse contains pagination metadata for list responses." + }, + "v1ParticipantProto": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "username": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "role": { + "type": "string" + }, + "isOnline": { + "type": "boolean" + }, + "joinedAt": { + "type": "string" + } + }, + "description": "ParticipantProto represents a user participating in a conversation." + }, + "v1PresenceEvent": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "isOnline": { + "type": "boolean" + } + }, + "description": "PresenceEvent represents a user online/offline status change." + }, + "v1ReadEvent": { + "type": "object", + "properties": { + "conversationId": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "readAt": { + "type": "string" + } + }, + "description": "ReadEvent represents a read receipt event." + }, + "v1ReadReceiptProto": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "readAt": { + "type": "string" + } + }, + "description": "ReadReceiptProto represents a read receipt for a message." + }, + "v1RemoveParticipantResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "RemoveParticipantResponse confirms a participant was removed." + }, + "v1SendMessageResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1MessageProto" + } + }, + "description": "SendMessageResponse returns the sent message." + }, + "v1SetTypingResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + } + }, + "description": "SetTypingResponse confirms the typing indicator was set." + }, + "v1StreamChatEventsResponse": { + "type": "object", + "properties": { + "eventId": { + "type": "string" + }, + "messageReceived": { + "$ref": "#/definitions/v1MessageEvent" + }, + "messageEdited": { + "$ref": "#/definitions/v1MessageEvent" + }, + "messageDeleted": { + "$ref": "#/definitions/v1DeleteEvent" + }, + "typing": { + "$ref": "#/definitions/v1TypingEvent" + }, + "readReceipt": { + "$ref": "#/definitions/v1ReadEvent" + }, + "presence": { + "$ref": "#/definitions/v1PresenceEvent" + } + }, + "description": "StreamChatEventsResponse wraps a real-time chat event delivered via server streaming." + }, + "v1TypingEvent": { + "type": "object", + "properties": { + "conversationId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "userName": { + "type": "string" + }, + "isTyping": { + "type": "boolean" + } + }, + "description": "TypingEvent represents a typing indicator event." + }, + "v1UpdateGroupConversationResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1ConversationProto" + } + }, + "description": "UpdateGroupConversationResponse returns the updated group conversation." + }, + "v1UploadChatAttachmentResponse": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/v1BaseResponse" + }, + "data": { + "$ref": "#/definitions/v1AttachmentProto" + } + }, + "description": "UploadChatAttachmentResponse returns the uploaded attachment metadata." + }, + "v1ValidationError": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "The field name that failed validation." + }, + "message": { + "type": "string", + "description": "The validation error message." + } + }, + "description": "ValidationError represents a single field validation error." + } + } +} diff --git a/services/finance/internal/delivery/grpc/interceptors.go b/services/finance/internal/delivery/grpc/interceptors.go index 24c3ea68..fd1ecda2 100644 --- a/services/finance/internal/delivery/grpc/interceptors.go +++ b/services/finance/internal/delivery/grpc/interceptors.go @@ -65,18 +65,35 @@ func RequestIDInterceptor() grpc.UnaryServerInterceptor { } } -// TimeoutInterceptor enforces request timeout. +// longRunningMethods maps gRPC method suffixes to extended timeouts for RPCs +// that routinely exceed the default 60s (batch compute, bulk import/export). +var longRunningMethods = map[string]time.Duration{ + "TriggerMbBatch": 5 * time.Minute, + "ExecutePushToHead": 5 * time.Minute, + "ProcessChunkInternal": 5 * time.Minute, + "ImportCostApplicableParams": 3 * time.Minute, + "ImportCostProductParameters": 3 * time.Minute, +} + +// TimeoutInterceptor enforces request timeout. RPCs listed in +// longRunningMethods get an extended deadline; all others use the default. func TimeoutInterceptor(timeout time.Duration) grpc.UnaryServerInterceptor { return func( ctx context.Context, req interface{}, - _ *grpc.UnaryServerInfo, + info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (interface{}, error) { - // Check if context already has deadline if _, ok := ctx.Deadline(); !ok { + effective := timeout + for suffix, dur := range longRunningMethods { + if strings.HasSuffix(info.FullMethod, "/"+suffix) { + effective = dur + break + } + } var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, timeout) + ctx, cancel = context.WithTimeout(ctx, effective) defer cancel() } diff --git a/services/iam/cmd/server/main.go b/services/iam/cmd/server/main.go index 75dd61fc..5ab54464 100644 --- a/services/iam/cmd/server/main.go +++ b/services/iam/cmd/server/main.go @@ -11,12 +11,17 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "encoding/hex" + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" authapp "github.com/mutugading/goapps-backend/services/iam/internal/application/auth" + appChat "github.com/mutugading/goapps-backend/services/iam/internal/application/chat" appnotif "github.com/mutugading/goapps-backend/services/iam/internal/application/notification" grpcdelivery "github.com/mutugading/goapps-backend/services/iam/internal/delivery/grpc" httpdelivery "github.com/mutugading/goapps-backend/services/iam/internal/delivery/httpdelivery" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/config" + iamcrypto "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" emailinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/email" "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/jwt" notifinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/notification" @@ -204,6 +209,16 @@ func run() error { notifStream, validationHelper, ).WithRequestHandler(notifRequestHandler) + // ── Chat + Presence ──────────────────────────────────────────────────── + chatHandler, presenceGRPCHandler := initChatServices(chatDeps{ + masterKeyHex: cfg.Chat.MasterKey, + db: db, + redisClient: redisClient, + notifCreate: notifCreate, + emailDispatcher: notifEmailDispatcher, + storageSvc: storageSvc, + }) + // Setup gRPC server with interceptor chain (pass JWT + session cache + session repo for auth & activity tracking) grpcServer, err := grpcdelivery.NewServer(&cfg.Server, db, jwtService, sessionCache, sessionRepo, cfg.Security.InternalServiceToken) if err != nil { @@ -232,6 +247,12 @@ func run() error { iamv1.RegisterNotificationServiceServer(gs, notificationHandler) iamv1.RegisterWorkflowTemplateServiceServer(gs, workflowTemplateHandler) iamv1.RegisterWorkflowInstanceServiceServer(gs, workflowInstanceHandler) + if chatHandler != nil { + iamv1.RegisterChatServiceServer(gs, chatHandler) + } + if presenceGRPCHandler != nil { + iamv1.RegisterPresenceServiceServer(gs, presenceGRPCHandler) + } // Start gRPC server go func() { @@ -273,6 +294,80 @@ func run() error { return nil } +// chatDeps bundles the dependencies needed to wire the chat + presence stack. +type chatDeps struct { + masterKeyHex string + db *postgres.DB + redisClient *redisinfra.Client + notifCreate *appnotif.CreateHandler + emailDispatcher appnotif.EmailDispatcher + storageSvc storageinfra.Service +} + +// initChatServices builds the chat and presence gRPC handlers. Both are nil +// when CHAT_MASTER_KEY is unset (chat disabled); presence is nil when Redis is +// unavailable. Extracted from run to keep its cognitive complexity in bounds. +func initChatServices(d chatDeps) (*grpcdelivery.ChatHandler, *grpcdelivery.PresenceHandler) { + if d.masterKeyHex == "" { + log.Warn().Msg("CHAT_MASTER_KEY not set — chat services disabled") + return nil, nil + } + chatMasterKey, hexErr := hex.DecodeString(d.masterKeyHex) + if hexErr != nil || len(chatMasterKey) != 32 { + log.Fatal().Msg("CHAT_MASTER_KEY must be a 64-char hex string (32 bytes)") + } + chatEnc, encErr := iamcrypto.NewEncryptor(chatMasterKey) + if encErr != nil { + log.Fatal().Err(encErr).Msg("Failed to init chat encryptor") + } + + chatBroadcaster := chatinfra.NewBroadcaster() + var presenceSvc *chatinfra.PresenceService + if d.redisClient != nil { + chatBroadcaster = chatinfra.NewRedisBroadcaster(d.redisClient.Client) + presenceSvc = chatinfra.NewPresenceService(d.redisClient.Client) + } + + convRepo := postgres.NewChatConversationRepository(d.db) + msgRepo := postgres.NewChatMessageRepository(d.db) + receiptRepo := postgres.NewChatReadReceiptRepository(d.db) + attRepo := postgres.NewChatAttachmentRepository(d.db) + userResolver := postgres.NewChatUserResolver(d.db) + + sendMsgHandler := appChat.NewSendMessageHandler(convRepo, msgRepo, receiptRepo, chatEnc, chatBroadcaster) + sendMsgHandler.WithAttachments(attRepo) + if presenceSvc != nil && d.redisClient != nil { + sendMsgHandler.WithOfflineNotification(presenceSvc, d.notifCreate, d.emailDispatcher, d.redisClient.Client, userResolver) + } + + chatHandler := grpcdelivery.NewChatHandler( + appChat.NewCreateDirectHandler(convRepo, chatEnc), + appChat.NewCreateGroupHandler(convRepo, chatEnc), + appChat.NewGetConversationHandler(convRepo), + appChat.NewListConversationsHandler(convRepo, msgRepo, chatEnc), + appChat.NewLeaveConversationHandler(convRepo), + sendMsgHandler, + appChat.NewEditMessageHandler(convRepo, msgRepo, chatEnc, chatBroadcaster, userResolver), + appChat.NewDeleteMessageHandler(convRepo, msgRepo, chatBroadcaster), + appChat.NewListMessagesHandler(convRepo, msgRepo, chatEnc), + appChat.NewMarkReadHandler(convRepo, msgRepo, receiptRepo, chatBroadcaster), + appChat.NewSetTypingHandler(convRepo, presenceSvc, chatBroadcaster), + appChat.NewStreamHandler(chatBroadcaster), + appChat.NewGetEditHistoryHandler(convRepo, msgRepo, chatEnc), + appChat.NewClearHistoryHandler(convRepo), + appChat.NewUploadAttachmentHandler(convRepo, attRepo, d.storageSvc), + attRepo, + userResolver, + ) + + var presenceGRPCHandler *grpcdelivery.PresenceHandler + if presenceSvc != nil { + presenceGRPCHandler = grpcdelivery.NewPresenceHandler(presenceSvc) + } + log.Info().Msg("Chat + Presence services initialized") + return chatHandler, presenceGRPCHandler +} + // setupLogger configures the application logger. func setupLogger() { zerolog.TimeFieldFormat = time.RFC3339 diff --git a/services/iam/config.yaml b/services/iam/config.yaml index 6981c965..095aaa5d 100644 --- a/services/iam/config.yaml +++ b/services/iam/config.yaml @@ -106,6 +106,9 @@ storage: region: us-east-1 public_url: "" # Override for external access URL, e.g., https://staging-goapps.mutugading.com/storage +chat: + master_key: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # 64-char hex (32 bytes) — dev only, override via CHAT_MASTER_KEY env in staging/prod + logging: level: debug # debug, info, warn, error format: console # console, json diff --git a/services/iam/internal/application/chat/add_participants_handler.go b/services/iam/internal/application/chat/add_participants_handler.go new file mode 100644 index 00000000..9b347c24 --- /dev/null +++ b/services/iam/internal/application/chat/add_participants_handler.go @@ -0,0 +1,45 @@ +// Package chat implements the application-layer use cases for chat +// conversations, messages, attachments, presence, and real-time events. +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// AddParticipantsHandler adds participants to a group conversation. +type AddParticipantsHandler struct { + convRepo chat.ConversationRepository +} + +// NewAddParticipantsHandler constructs the handler. +func NewAddParticipantsHandler(convRepo chat.ConversationRepository) *AddParticipantsHandler { + return &AddParticipantsHandler{convRepo: convRepo} +} + +// Handle adds userIDs to the conversation. Caller must be ADMIN or OWNER. +func (h *AddParticipantsHandler) Handle(ctx context.Context, callerID, convID uuid.UUID, userIDs []uuid.UUID) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return fmt.Errorf("add participants: %w", chat.ErrNotParticipant) + } + if !p.Role().IsAdminOrOwner() { + return fmt.Errorf("add participants: %w", chat.ErrNotAdmin) + } + newParts := make([]*chat.Participant, 0, len(userIDs)) + for _, uid := range userIDs { + if err := conv.AddParticipant(uid, chat.RoleMember); err != nil { + return fmt.Errorf("add participants: %w", err) + } + newParts = append(newParts, conv.FindParticipant(uid)) + } + return h.convRepo.AddParticipants(ctx, convID, newParts) +} diff --git a/services/iam/internal/application/chat/broadcast_helpers.go b/services/iam/internal/application/chat/broadcast_helpers.go new file mode 100644 index 00000000..89e1cd97 --- /dev/null +++ b/services/iam/internal/application/chat/broadcast_helpers.go @@ -0,0 +1,104 @@ +package chat + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + domainChat "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/postgres" +) + +// decryptionErrorBody is the placeholder body shown when a message body +// fails to decrypt (e.g. master key rotated out from under an old message). +const decryptionErrorBody = "[decryption error]" + +// resolveSenderName looks up a display name for senderID via userResolver, +// falling back to username, then an empty string if the resolver is nil or +// the lookup fails. +func resolveSenderName(ctx context.Context, userResolver *postgres.ChatUserResolver, senderID uuid.UUID) string { + if userResolver == nil { + return "" + } + infos, err := userResolver.ResolveUsers(ctx, []uuid.UUID{senderID}) + if err != nil { + log.Warn().Err(err).Str("user", senderID.String()).Msg("chat: resolve sender name") + return "" + } + info := infos[senderID] + if info == nil { + return "" + } + if info.FullName != "" { + return info.FullName + } + return info.Username +} + +// broadcastMessageEvent publishes a message-related event (created, edited) to +// every active participant of conv via the given broadcaster. +func broadcastMessageEvent(broadcaster *chatinfra.Broadcaster, conv *domainChat.Conversation, msg *domainChat.Message, plainBody, eventType, senderName string, attachments []*iamv1.AttachmentProto) { + msgProto := domainMsgToProto(msg, plainBody) + msgProto.SenderName = senderName + msgProto.Attachments = attachments + eventID := fmt.Sprintf("msg-%s", msg.MessageID()) + + var resp iamv1.StreamChatEventsResponse + resp.EventId = eventID + switch eventType { + case "message_received": + resp.Payload = &iamv1.StreamChatEventsResponse_MessageReceived{ + MessageReceived: &iamv1.MessageEvent{ConversationId: conv.ID().String(), Message: msgProto}, + } + case "message_edited": + resp.Payload = &iamv1.StreamChatEventsResponse_MessageEdited{ + MessageEdited: &iamv1.MessageEvent{ConversationId: conv.ID().String(), Message: msgProto}, + } + } + + for _, part := range conv.Participants() { + if !part.IsActive() { + continue + } + broadcaster.Publish(&chatinfra.Event{ + EventID: fmt.Sprintf("%s-%s", eventID, part.UserID()), + UserID: part.UserID(), + Response: &resp, + }) + } +} + +// attachmentToProto maps a domain attachment to its proto representation. +func attachmentToProto(a *domainChat.Attachment) *iamv1.AttachmentProto { + return &iamv1.AttachmentProto{ + AttachmentId: a.AttachmentID().String(), + FileName: a.FileName(), + FileUrl: a.FileURL(), + ContentType: a.ContentType(), + FileSize: a.FileSize(), + ThumbnailUrl: a.ThumbnailURL(), + } +} + +func domainMsgToProto(msg *domainChat.Message, plainBody string) *iamv1.MessageProto { + replyTo := "" + if msg.ReplyToID() != uuid.Nil { + replyTo = msg.ReplyToID().String() + } + return &iamv1.MessageProto{ + MessageId: msg.MessageID().String(), + ConversationId: msg.ConversationID().String(), + SenderUserId: msg.SenderUserID().String(), + Body: plainBody, + IsEdited: msg.IsEdited(), + IsDeleted: msg.IsDeleted(), + ReplyToId: replyTo, + CreatedAt: msg.CreatedAt().UTC().Format(time.RFC3339Nano), + UpdatedAt: msg.UpdatedAt().UTC().Format(time.RFC3339Nano), + } +} diff --git a/services/iam/internal/application/chat/clear_history_handler.go b/services/iam/internal/application/chat/clear_history_handler.go new file mode 100644 index 00000000..80182027 --- /dev/null +++ b/services/iam/internal/application/chat/clear_history_handler.go @@ -0,0 +1,34 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// ClearHistoryHandler clears the calling user's own view of a conversation's +// message history. Messages remain visible to other participants. +type ClearHistoryHandler struct { + convRepo chat.ConversationRepository +} + +// NewClearHistoryHandler constructs the handler. +func NewClearHistoryHandler(convRepo chat.ConversationRepository) *ClearHistoryHandler { + return &ClearHistoryHandler{convRepo: convRepo} +} + +// Handle verifies callerID is an active participant, then clears their history. +func (h *ClearHistoryHandler) Handle(ctx context.Context, callerID, convID uuid.UUID) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return fmt.Errorf("clear history: %w", chat.ErrNotParticipant) + } + return h.convRepo.ClearHistory(ctx, convID, callerID) +} diff --git a/services/iam/internal/application/chat/create_direct_handler.go b/services/iam/internal/application/chat/create_direct_handler.go new file mode 100644 index 00000000..65873b7f --- /dev/null +++ b/services/iam/internal/application/chat/create_direct_handler.go @@ -0,0 +1,51 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" +) + +// CreateDirectHandler creates a 1:1 conversation (idempotent — returns existing if present). +type CreateDirectHandler struct { + convRepo chat.ConversationRepository + enc *crypto.Encryptor +} + +// NewCreateDirectHandler constructs the handler. +func NewCreateDirectHandler(convRepo chat.ConversationRepository, enc *crypto.Encryptor) *CreateDirectHandler { + return &CreateDirectHandler{convRepo: convRepo, enc: enc} +} + +// Handle creates or returns an existing DIRECT conversation between callerID and peerID. +func (h *CreateDirectHandler) Handle(ctx context.Context, callerID, peerID uuid.UUID) (*chat.Conversation, error) { + existing, err := h.convRepo.FindDirect(ctx, callerID, peerID) + if err != nil { + return nil, fmt.Errorf("create direct: find existing: %w", err) + } + if existing != nil { + return existing, nil + } + + convKeyPlain, err := h.enc.GenerateConversationKey() + if err != nil { + return nil, fmt.Errorf("create direct: generate key: %w", err) + } + encKey, err := h.enc.EncryptConversationKey(convKeyPlain) + if err != nil { + return nil, fmt.Errorf("create direct: encrypt key: %w", err) + } + + conv, err := chat.NewDirectConversation(callerID, peerID, convKeyPlain, encKey) + if err != nil { + return nil, fmt.Errorf("create direct: build: %w", err) + } + if err := h.convRepo.Create(ctx, conv); err != nil { + return nil, fmt.Errorf("create direct: persist: %w", err) + } + return conv, nil +} diff --git a/services/iam/internal/application/chat/create_group_handler.go b/services/iam/internal/application/chat/create_group_handler.go new file mode 100644 index 00000000..8ac6b926 --- /dev/null +++ b/services/iam/internal/application/chat/create_group_handler.go @@ -0,0 +1,42 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" +) + +// CreateGroupHandler creates a new group conversation. +type CreateGroupHandler struct { + convRepo chat.ConversationRepository + enc *crypto.Encryptor +} + +// NewCreateGroupHandler constructs the handler. +func NewCreateGroupHandler(convRepo chat.ConversationRepository, enc *crypto.Encryptor) *CreateGroupHandler { + return &CreateGroupHandler{convRepo: convRepo, enc: enc} +} + +// Handle creates a group conversation with the caller as OWNER. +func (h *CreateGroupHandler) Handle(ctx context.Context, ownerID uuid.UUID, name string, memberIDs []uuid.UUID) (*chat.Conversation, error) { + convKeyPlain, err := h.enc.GenerateConversationKey() + if err != nil { + return nil, fmt.Errorf("create group: generate key: %w", err) + } + encKey, err := h.enc.EncryptConversationKey(convKeyPlain) + if err != nil { + return nil, fmt.Errorf("create group: encrypt key: %w", err) + } + conv, err := chat.NewGroupConversation(ownerID, name, memberIDs, convKeyPlain, encKey) + if err != nil { + return nil, fmt.Errorf("create group: build: %w", err) + } + if err := h.convRepo.Create(ctx, conv); err != nil { + return nil, fmt.Errorf("create group: persist: %w", err) + } + return conv, nil +} diff --git a/services/iam/internal/application/chat/delete_message_handler.go b/services/iam/internal/application/chat/delete_message_handler.go new file mode 100644 index 00000000..41d2a93d --- /dev/null +++ b/services/iam/internal/application/chat/delete_message_handler.go @@ -0,0 +1,70 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" +) + +// DeleteMessageHandler soft-deletes a message. +type DeleteMessageHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + broadcaster *chatinfra.Broadcaster +} + +// NewDeleteMessageHandler constructs the handler. +func NewDeleteMessageHandler(convRepo chat.ConversationRepository, msgRepo chat.MessageRepository, broadcaster *chatinfra.Broadcaster) *DeleteMessageHandler { + return &DeleteMessageHandler{convRepo: convRepo, msgRepo: msgRepo, broadcaster: broadcaster} +} + +// Handle soft-deletes the message. Only author or conversation ADMIN/OWNER. +func (h *DeleteMessageHandler) Handle(ctx context.Context, callerID, convID, msgID uuid.UUID) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + msg, err := h.msgRepo.GetByID(ctx, msgID) + if err != nil { + return err + } + if msg.SenderUserID() != callerID { + p := conv.FindParticipant(callerID) + if p == nil || !p.Role().IsAdminOrOwner() { + return fmt.Errorf("delete message: %w", chat.ErrNotAuthor) + } + } + if err := h.msgRepo.MarkDeleted(ctx, msgID); err != nil { + return fmt.Errorf("delete message: %w", err) + } + h.broadcastDelete(conv, convID, msgID) + return nil +} + +func (h *DeleteMessageHandler) broadcastDelete(conv *chat.Conversation, convID, msgID uuid.UUID) { + eventID := fmt.Sprintf("del-%s", msgID) + resp := &iamv1.StreamChatEventsResponse{ + EventId: eventID, + Payload: &iamv1.StreamChatEventsResponse_MessageDeleted{ + MessageDeleted: &iamv1.DeleteEvent{ + ConversationId: convID.String(), + MessageId: msgID.String(), + }, + }, + } + for _, p := range conv.Participants() { + if !p.IsActive() { + continue + } + h.broadcaster.Publish(&chatinfra.Event{ + EventID: fmt.Sprintf("%s-%s", eventID, p.UserID()), + UserID: p.UserID(), + Response: resp, + }) + } +} diff --git a/services/iam/internal/application/chat/edit_message_handler.go b/services/iam/internal/application/chat/edit_message_handler.go new file mode 100644 index 00000000..504febc8 --- /dev/null +++ b/services/iam/internal/application/chat/edit_message_handler.go @@ -0,0 +1,78 @@ +package chat + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/postgres" +) + +// EditMessageHandler handles message edits. +type EditMessageHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + enc *crypto.Encryptor + broadcaster *chatinfra.Broadcaster + userResolver *postgres.ChatUserResolver +} + +// NewEditMessageHandler constructs the handler. +func NewEditMessageHandler(convRepo chat.ConversationRepository, msgRepo chat.MessageRepository, enc *crypto.Encryptor, broadcaster *chatinfra.Broadcaster, userResolver *postgres.ChatUserResolver) *EditMessageHandler { + return &EditMessageHandler{convRepo: convRepo, msgRepo: msgRepo, enc: enc, broadcaster: broadcaster, userResolver: userResolver} +} + +// Handle edits a message. Only the author can edit; saves edit history. +func (h *EditMessageHandler) Handle(ctx context.Context, callerID, convID, msgID uuid.UUID, newBody string) (*chat.Message, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, err + } + msg, err := h.msgRepo.GetByID(ctx, msgID) + if err != nil { + return nil, err + } + if msg.IsDeleted() { + return nil, fmt.Errorf("edit message: %w", chat.ErrMessageDeleted) + } + if msg.SenderUserID() != callerID { + p := conv.FindParticipant(callerID) + if p == nil || !p.Role().IsAdminOrOwner() { + return nil, fmt.Errorf("edit message: %w", chat.ErrNotAuthor) + } + } + + convKeyPlain, err := h.enc.DecryptConversationKey(conv.EncryptionKey()) + if err != nil { + return nil, fmt.Errorf("edit message: decrypt conv key: %w", err) + } + + // Save edit history snapshot before mutating. + histEntry := chat.ReconstructEditHistory(0, msgID, msg.BodyEncrypted(), callerID, time.Now().UTC()) + if saveErr := h.msgRepo.SaveEditHistory(ctx, histEntry); saveErr != nil { + log.Warn().Err(saveErr).Msg("edit message: save history") + } + + newBodyEnc, err := h.enc.EncryptMessage(convKeyPlain, newBody) + if err != nil { + return nil, fmt.Errorf("edit message: encrypt new body: %w", err) + } + newBodyPlainEnc, err := h.enc.EncryptMessage(convKeyPlain, newBody) + if err != nil { + return nil, fmt.Errorf("edit message: encrypt plain: %w", err) + } + msg.Edit(newBodyEnc, newBodyPlainEnc) + + if err := h.msgRepo.UpdateBody(ctx, msg); err != nil { + return nil, fmt.Errorf("edit message: update: %w", err) + } + senderName := resolveSenderName(ctx, h.userResolver, msg.SenderUserID()) + broadcastMessageEvent(h.broadcaster, conv, msg, newBody, "message_edited", senderName, nil) + return msg, nil +} diff --git a/services/iam/internal/application/chat/get_conversation_handler.go b/services/iam/internal/application/chat/get_conversation_handler.go new file mode 100644 index 00000000..6fa80a17 --- /dev/null +++ b/services/iam/internal/application/chat/get_conversation_handler.go @@ -0,0 +1,33 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// GetConversationHandler fetches a conversation, enforcing participant check. +type GetConversationHandler struct { + convRepo chat.ConversationRepository +} + +// NewGetConversationHandler constructs the handler. +func NewGetConversationHandler(convRepo chat.ConversationRepository) *GetConversationHandler { + return &GetConversationHandler{convRepo: convRepo} +} + +// Handle returns the conversation if callerID is an active participant. +func (h *GetConversationHandler) Handle(ctx context.Context, callerID, convID uuid.UUID) (*chat.Conversation, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return nil, fmt.Errorf("get conversation: %w", chat.ErrNotParticipant) + } + return conv, nil +} diff --git a/services/iam/internal/application/chat/get_edit_history_handler.go b/services/iam/internal/application/chat/get_edit_history_handler.go new file mode 100644 index 00000000..9050b842 --- /dev/null +++ b/services/iam/internal/application/chat/get_edit_history_handler.go @@ -0,0 +1,64 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" +) + +// GetEditHistoryHandler returns the decrypted edit history for a message. +type GetEditHistoryHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + enc *crypto.Encryptor +} + +// NewGetEditHistoryHandler constructs the handler. +func NewGetEditHistoryHandler(convRepo chat.ConversationRepository, msgRepo chat.MessageRepository, enc *crypto.Encryptor) *GetEditHistoryHandler { + return &GetEditHistoryHandler{convRepo: convRepo, msgRepo: msgRepo, enc: enc} +} + +// EditHistoryEntryResult pairs an edit history entry with its decrypted body. +type EditHistoryEntryResult struct { + *chat.EditHistoryEntry + PlainBody string +} + +// Handle returns the edit history for messageID, newest first. callerID must +// be an active participant of convID. +func (h *GetEditHistoryHandler) Handle(ctx context.Context, callerID, convID, messageID uuid.UUID) ([]*EditHistoryEntryResult, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return nil, fmt.Errorf("get edit history: %w", chat.ErrNotParticipant) + } + + entries, err := h.msgRepo.GetEditHistory(ctx, messageID) + if err != nil { + return nil, fmt.Errorf("get edit history: %w", err) + } + + convKeyPlain, err := h.enc.DecryptConversationKey(conv.EncryptionKey()) + if err != nil { + log.Warn().Err(err).Str("conv", convID.String()).Msg("get edit history: decrypt conv key failed") + return nil, fmt.Errorf("get edit history: decrypt conv key: %w", err) + } + + results := make([]*EditHistoryEntryResult, 0, len(entries)) + for _, entry := range entries { + plain, decErr := h.enc.DecryptMessage(convKeyPlain, entry.BodyEncrypted()) + if decErr != nil { + plain = decryptionErrorBody + } + results = append(results, &EditHistoryEntryResult{EditHistoryEntry: entry, PlainBody: plain}) + } + return results, nil +} diff --git a/services/iam/internal/application/chat/leave_conversation_handler.go b/services/iam/internal/application/chat/leave_conversation_handler.go new file mode 100644 index 00000000..517a4c56 --- /dev/null +++ b/services/iam/internal/application/chat/leave_conversation_handler.go @@ -0,0 +1,33 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// LeaveConversationHandler removes the calling user from a conversation. +type LeaveConversationHandler struct { + convRepo chat.ConversationRepository +} + +// NewLeaveConversationHandler constructs the handler. +func NewLeaveConversationHandler(convRepo chat.ConversationRepository) *LeaveConversationHandler { + return &LeaveConversationHandler{convRepo: convRepo} +} + +// Handle removes callerID from the conversation. +func (h *LeaveConversationHandler) Handle(ctx context.Context, callerID, convID uuid.UUID) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return fmt.Errorf("leave conversation: %w", chat.ErrNotParticipant) + } + return h.convRepo.RemoveParticipant(ctx, convID, callerID) +} diff --git a/services/iam/internal/application/chat/list_conversations_handler.go b/services/iam/internal/application/chat/list_conversations_handler.go new file mode 100644 index 00000000..7b7ab365 --- /dev/null +++ b/services/iam/internal/application/chat/list_conversations_handler.go @@ -0,0 +1,86 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" +) + +// ListConversationsHandler lists conversations for the calling user. +type ListConversationsHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + enc *crypto.Encryptor +} + +// NewListConversationsHandler constructs the handler. +func NewListConversationsHandler(convRepo chat.ConversationRepository, msgRepo chat.MessageRepository, enc *crypto.Encryptor) *ListConversationsHandler { + return &ListConversationsHandler{convRepo: convRepo, msgRepo: msgRepo, enc: enc} +} + +// ConversationSummary pairs a conversation with its decrypted last message +// and the caller's unread count. +type ConversationSummary struct { + Conversation *chat.Conversation + LastMessage *chat.Message + LastMessageBody string + UnreadCount int32 +} + +// ListResult holds the list result with pagination. +type ListResult struct { + Conversations []*ConversationSummary + Total int64 +} + +// Handle returns paginated conversations for callerID, enriched with last +// message + unread count. Both are batch-loaded (one query each), not N+1. +func (h *ListConversationsHandler) Handle(ctx context.Context, callerID uuid.UUID, page, pageSize int) (*ListResult, error) { + convs, total, err := h.convRepo.ListByUserID(ctx, callerID, page, pageSize) + if err != nil { + return nil, fmt.Errorf("list conversations: %w", err) + } + + convIDs := make([]uuid.UUID, len(convs)) + for i, c := range convs { + convIDs[i] = c.ID() + } + + lastMsgs, err := h.msgRepo.GetLastMessages(ctx, convIDs, callerID) + if err != nil { + return nil, fmt.Errorf("list conversations: last messages: %w", err) + } + unreadCounts, err := h.convRepo.GetUnreadCounts(ctx, convIDs, callerID) + if err != nil { + return nil, fmt.Errorf("list conversations: unread counts: %w", err) + } + + summaries := make([]*ConversationSummary, 0, len(convs)) + for _, c := range convs { + summary := &ConversationSummary{Conversation: c, UnreadCount: unreadCounts[c.ID()]} + if lastMsg := lastMsgs[c.ID()]; lastMsg != nil { + summary.LastMessage = lastMsg + summary.LastMessageBody = h.decryptLastMessage(c, lastMsg) + } + summaries = append(summaries, summary) + } + return &ListResult{Conversations: summaries, Total: total}, nil +} + +func (h *ListConversationsHandler) decryptLastMessage(conv *chat.Conversation, msg *chat.Message) string { + convKeyPlain, err := h.enc.DecryptConversationKey(conv.EncryptionKey()) + if err != nil { + log.Warn().Err(err).Str("conv", conv.ID().String()).Msg("list conversations: decrypt conv key failed") + return "" + } + plain, err := h.enc.DecryptMessage(convKeyPlain, msg.BodyEncrypted()) + if err != nil { + return decryptionErrorBody + } + return plain +} diff --git a/services/iam/internal/application/chat/list_messages_handler.go b/services/iam/internal/application/chat/list_messages_handler.go new file mode 100644 index 00000000..af7b2edc --- /dev/null +++ b/services/iam/internal/application/chat/list_messages_handler.go @@ -0,0 +1,74 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" +) + +// ListMessagesHandler lists messages for a conversation, decrypting bodies. +type ListMessagesHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + enc *crypto.Encryptor +} + +// NewListMessagesHandler constructs the handler. +func NewListMessagesHandler(convRepo chat.ConversationRepository, msgRepo chat.MessageRepository, enc *crypto.Encryptor) *ListMessagesHandler { + return &ListMessagesHandler{convRepo: convRepo, msgRepo: msgRepo, enc: enc} +} + +// DecryptedMessage pairs a domain message with its decrypted body. +type DecryptedMessage struct { + *chat.Message + PlainBody string +} + +// MessagesResult holds the paginated message list. +type MessagesResult struct { + Messages []*DecryptedMessage + NextCursor string + HasMore bool +} + +// Handle fetches and decrypts messages. callerID must be an active participant. +func (h *ListMessagesHandler) Handle(ctx context.Context, callerID, convID uuid.UUID, pageSize int, beforeCursor string) (*MessagesResult, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return nil, fmt.Errorf("list messages: %w", chat.ErrNotParticipant) + } + + convKeyPlain, err := h.enc.DecryptConversationKey(conv.EncryptionKey()) + if err != nil { + log.Warn().Err(err).Str("conv", convID.String()).Msg("list messages: decrypt conv key failed (master key may have changed)") + return &MessagesResult{Messages: []*DecryptedMessage{}, NextCursor: "", HasMore: false}, nil + } + + msgs, nextCursor, hasMore, err := h.msgRepo.ListByConversation(ctx, convID, pageSize, beforeCursor, p.HistoryClearedAt()) + if err != nil { + return nil, fmt.Errorf("list messages: %w", err) + } + + decrypted := make([]*DecryptedMessage, 0, len(msgs)) + for _, msg := range msgs { + plain := "[deleted]" + if !msg.IsDeleted() { + var decErr error + plain, decErr = h.enc.DecryptMessage(convKeyPlain, msg.BodyEncrypted()) + if decErr != nil { + plain = decryptionErrorBody + } + } + decrypted = append(decrypted, &DecryptedMessage{Message: msg, PlainBody: plain}) + } + return &MessagesResult{Messages: decrypted, NextCursor: nextCursor, HasMore: hasMore}, nil +} diff --git a/services/iam/internal/application/chat/mark_read_handler.go b/services/iam/internal/application/chat/mark_read_handler.go new file mode 100644 index 00000000..d86d2c04 --- /dev/null +++ b/services/iam/internal/application/chat/mark_read_handler.go @@ -0,0 +1,95 @@ +package chat + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" +) + +// MarkReadHandler marks all unread messages in a conversation as read. +type MarkReadHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + receiptRepo chat.ReadReceiptRepository + broadcaster *chatinfra.Broadcaster +} + +// NewMarkReadHandler constructs the handler. +func NewMarkReadHandler( + convRepo chat.ConversationRepository, + msgRepo chat.MessageRepository, + receiptRepo chat.ReadReceiptRepository, + broadcaster *chatinfra.Broadcaster, +) *MarkReadHandler { + return &MarkReadHandler{convRepo: convRepo, msgRepo: msgRepo, receiptRepo: receiptRepo, broadcaster: broadcaster} +} + +// Handle upserts read receipts for recent messages and updates last_read_at. +func (h *MarkReadHandler) Handle(ctx context.Context, callerID, convID uuid.UUID) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return fmt.Errorf("mark read: %w", chat.ErrNotParticipant) + } + + // Fetch recent messages (up to last 100) and upsert receipts. Messages the + // caller has cleared from their own view are excluded — no point marking + // hidden history as read. + msgs, _, _, err := h.msgRepo.ListByConversation(ctx, convID, 100, "", p.HistoryClearedAt()) + if err != nil { + return fmt.Errorf("mark read: list: %w", err) + } + + ids := make([]uuid.UUID, 0, len(msgs)) + for _, m := range msgs { + if !m.IsDeleted() { + ids = append(ids, m.MessageID()) + } + } + if err := h.receiptRepo.UpsertBulk(ctx, ids, callerID); err != nil { + return fmt.Errorf("mark read: upsert receipts: %w", err) + } + + now := time.Now().UTC() + if err := h.convRepo.UpdateLastReadAt(ctx, convID, callerID, now); err != nil { + log.Warn().Err(err).Msg("mark read: update last_read_at") + } + + // Broadcast read receipt event to conversation participants. + h.broadcastRead(conv, callerID, now) + return nil +} + +func (h *MarkReadHandler) broadcastRead(conv *chat.Conversation, readerID uuid.UUID, readAt time.Time) { + eventID := fmt.Sprintf("read-%s-%s", conv.ID(), readerID) + resp := &iamv1.StreamChatEventsResponse{ + EventId: eventID, + Payload: &iamv1.StreamChatEventsResponse_ReadReceipt{ + ReadReceipt: &iamv1.ReadEvent{ + ConversationId: conv.ID().String(), + UserId: readerID.String(), + ReadAt: readAt.Format(time.RFC3339Nano), + }, + }, + } + for _, p := range conv.Participants() { + if !p.IsActive() { + continue + } + h.broadcaster.Publish(&chatinfra.Event{ + EventID: fmt.Sprintf("%s-%s", eventID, p.UserID()), + UserID: p.UserID(), + Response: resp, + }) + } +} diff --git a/services/iam/internal/application/chat/remove_participant_handler.go b/services/iam/internal/application/chat/remove_participant_handler.go new file mode 100644 index 00000000..c4cbb3c5 --- /dev/null +++ b/services/iam/internal/application/chat/remove_participant_handler.go @@ -0,0 +1,40 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// RemoveParticipantHandler removes a participant from a group conversation. +type RemoveParticipantHandler struct { + convRepo chat.ConversationRepository +} + +// NewRemoveParticipantHandler constructs the handler. +func NewRemoveParticipantHandler(convRepo chat.ConversationRepository) *RemoveParticipantHandler { + return &RemoveParticipantHandler{convRepo: convRepo} +} + +// Handle removes targetUserID from the conversation. Caller must be ADMIN/OWNER. +func (h *RemoveParticipantHandler) Handle(ctx context.Context, callerID, convID, targetUserID uuid.UUID) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return fmt.Errorf("remove participant: %w", chat.ErrNotParticipant) + } + if !p.Role().IsAdminOrOwner() { + return fmt.Errorf("remove participant: %w", chat.ErrNotAdmin) + } + target := conv.FindParticipant(targetUserID) + if target == nil || !target.IsActive() { + return fmt.Errorf("remove participant: target %w", chat.ErrNotParticipant) + } + return h.convRepo.RemoveParticipant(ctx, convID, targetUserID) +} diff --git a/services/iam/internal/application/chat/send_message_handler.go b/services/iam/internal/application/chat/send_message_handler.go new file mode 100644 index 00000000..7a634a88 --- /dev/null +++ b/services/iam/internal/application/chat/send_message_handler.go @@ -0,0 +1,208 @@ +package chat + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "github.com/rs/zerolog/log" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + appnotif "github.com/mutugading/goapps-backend/services/iam/internal/application/notification" + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/domain/notification" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/postgres" +) + +// SendMessageHandler sends a new message to a conversation. +type SendMessageHandler struct { + convRepo chat.ConversationRepository + msgRepo chat.MessageRepository + receiptRepo chat.ReadReceiptRepository + enc *crypto.Encryptor + broadcaster *chatinfra.Broadcaster + presence *chatinfra.PresenceService + notifCreate *appnotif.CreateHandler + emailDispatch appnotif.EmailDispatcher + rdb *redis.Client + userResolver *postgres.ChatUserResolver + attRepo chat.AttachmentRepository +} + +// NewSendMessageHandler constructs the handler. +func NewSendMessageHandler( + convRepo chat.ConversationRepository, + msgRepo chat.MessageRepository, + receiptRepo chat.ReadReceiptRepository, + enc *crypto.Encryptor, + broadcaster *chatinfra.Broadcaster, +) *SendMessageHandler { + return &SendMessageHandler{convRepo: convRepo, msgRepo: msgRepo, receiptRepo: receiptRepo, enc: enc, broadcaster: broadcaster} +} + +// WithAttachments enables linking uploaded attachments to sent messages. +func (h *SendMessageHandler) WithAttachments(attRepo chat.AttachmentRepository) *SendMessageHandler { + h.attRepo = attRepo + return h +} + +// WithOfflineNotification enables email notifications for offline users. +func (h *SendMessageHandler) WithOfflineNotification(presence *chatinfra.PresenceService, notifCreate *appnotif.CreateHandler, emailDispatch appnotif.EmailDispatcher, rdb *redis.Client, userResolver *postgres.ChatUserResolver) *SendMessageHandler { + h.presence = presence + h.notifCreate = notifCreate + h.emailDispatch = emailDispatch + h.rdb = rdb + h.userResolver = userResolver + return h +} + +// Handle validates participation, encrypts body, saves, and broadcasts. Any +// attachmentIDs that belong to this conversation and are not yet linked are +// attached to the new message. Returns the message plus its attachment protos. +func (h *SendMessageHandler) Handle(ctx context.Context, senderID, convID uuid.UUID, body string, replyToID uuid.UUID, attachmentIDs []uuid.UUID) (*chat.Message, []*iamv1.AttachmentProto, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, nil, err + } + p := conv.FindParticipant(senderID) + if p == nil || !p.IsActive() { + return nil, nil, fmt.Errorf("send message: %w", chat.ErrNotParticipant) + } + + convKeyPlain, err := h.enc.DecryptConversationKey(conv.EncryptionKey()) + if err != nil { + return nil, nil, fmt.Errorf("send message: decrypt conv key: %w", err) + } + + bodyEnc, err := h.enc.EncryptMessage(convKeyPlain, body) + if err != nil { + return nil, nil, fmt.Errorf("send message: encrypt body: %w", err) + } + bodyPlainEnc, err := h.enc.EncryptMessage(convKeyPlain, body) // same body, independent nonce for search index slot + if err != nil { + return nil, nil, fmt.Errorf("send message: encrypt plain: %w", err) + } + + msg := chat.NewMessage(convID, senderID, bodyEnc, bodyPlainEnc, replyToID) + if err := h.msgRepo.Create(ctx, msg); err != nil { + return nil, nil, fmt.Errorf("send message: persist: %w", err) + } + + // Auto-read for sender. + if err := h.receiptRepo.Upsert(ctx, msg.MessageID(), senderID); err != nil { + log.Warn().Err(err).Msg("send message: auto read receipt failed") + } + + attProtos, err := h.linkAttachments(ctx, convID, msg.MessageID(), attachmentIDs) + if err != nil { + return nil, nil, err + } + + senderName := resolveSenderName(ctx, h.userResolver, senderID) + broadcastMessageEvent(h.broadcaster, conv, msg, body, "message_received", senderName, attProtos) + + h.notifyOfflineParticipants(ctx, conv, msg, senderID, senderName, body) + return msg, attProtos, nil +} + +// linkAttachments links pending attachments (belonging to convID and not yet +// linked) to messageID and returns their proto representations. +func (h *SendMessageHandler) linkAttachments(ctx context.Context, convID, messageID uuid.UUID, attachmentIDs []uuid.UUID) ([]*iamv1.AttachmentProto, error) { + if len(attachmentIDs) == 0 || h.attRepo == nil { + return nil, nil + } + found, err := h.attRepo.GetByIDs(ctx, attachmentIDs) + if err != nil { + return nil, fmt.Errorf("send message: load attachments: %w", err) + } + validIDs := make([]uuid.UUID, 0, len(found)) + protos := make([]*iamv1.AttachmentProto, 0, len(found)) + for _, att := range found { + if att.ConversationID() != convID || att.MessageID() != nil { + continue + } + validIDs = append(validIDs, att.AttachmentID()) + protos = append(protos, attachmentToProto(att)) + } + if len(validIDs) == 0 { + return nil, nil + } + if err := h.attRepo.LinkToMessage(ctx, messageID, validIDs); err != nil { + return nil, fmt.Errorf("send message: link attachments: %w", err) + } + return protos, nil +} + +const emailDebounceTTL = 5 * time.Minute + +func (h *SendMessageHandler) notifyOfflineParticipants(ctx context.Context, conv *chat.Conversation, msg *chat.Message, senderID uuid.UUID, senderName, body string) { + if h.presence == nil || h.notifCreate == nil { + return + } + truncatedBody := body + if len(truncatedBody) > 100 { + truncatedBody = truncatedBody[:100] + "..." + } + for _, p := range conv.Participants() { + if !p.IsActive() || p.UserID() == senderID { + continue + } + h.notifyIfOffline(ctx, p.UserID(), msg, senderID, senderName, truncatedBody) + } +} + +func (h *SendMessageHandler) notifyIfOffline(ctx context.Context, recipientID uuid.UUID, msg *chat.Message, senderID uuid.UUID, senderName, truncatedBody string) { + online, err := h.presence.IsOnline(ctx, recipientID) + if err != nil { + log.Warn().Err(err).Str("user", recipientID.String()).Msg("send message: check online status") + return + } + if online { + return + } + if h.isDebounced(ctx, msg.ConversationID(), recipientID) { + return + } + title := "New chat message" + body := truncatedBody + if senderName != "" { + title = fmt.Sprintf("%s sent you a message", senderName) + body = fmt.Sprintf("%s: %s", senderName, truncatedBody) + } + n, err := h.notifCreate.Handle(ctx, appnotif.CreateCommand{ + RecipientUserID: recipientID, + Type: notification.TypeChat, + Severity: notification.SeverityInfo, + Title: title, + Body: body, + ActionType: notification.ActionNavigate, + ActionPayload: `{"path":"/chat"}`, + SourceType: "chat_message", + SourceID: msg.MessageID().String(), + CreatedBy: senderID.String(), + }) + if err != nil { + log.Warn().Err(err).Msg("send message: create offline notification") + return + } + if h.emailDispatch != nil && n != nil { + go h.emailDispatch.Dispatch(context.Background(), n) + } +} + +func (h *SendMessageHandler) isDebounced(ctx context.Context, convID, userID uuid.UUID) bool { + if h.rdb == nil { + return false + } + key := fmt.Sprintf("chat:email-debounce:%s:%s", convID, userID) + set, err := h.rdb.SetNX(ctx, key, "1", emailDebounceTTL).Result() + if err != nil { + log.Warn().Err(err).Msg("send message: debounce check") + return false + } + return !set +} diff --git a/services/iam/internal/application/chat/set_typing_handler.go b/services/iam/internal/application/chat/set_typing_handler.go new file mode 100644 index 00000000..91d327d4 --- /dev/null +++ b/services/iam/internal/application/chat/set_typing_handler.go @@ -0,0 +1,63 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" +) + +// SetTypingHandler publishes typing indicator events. +type SetTypingHandler struct { + convRepo chat.ConversationRepository + presence *chatinfra.PresenceService + broadcaster *chatinfra.Broadcaster +} + +// NewSetTypingHandler constructs the handler. +func NewSetTypingHandler(convRepo chat.ConversationRepository, presence *chatinfra.PresenceService, broadcaster *chatinfra.Broadcaster) *SetTypingHandler { + return &SetTypingHandler{convRepo: convRepo, presence: presence, broadcaster: broadcaster} +} + +// Handle sets/clears typing indicator and broadcasts to other participants. +func (h *SetTypingHandler) Handle(ctx context.Context, callerID, convID uuid.UUID, isTyping bool, callerName string) error { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return fmt.Errorf("set typing: %w", chat.ErrNotParticipant) + } + if err := h.presence.SetTyping(ctx, convID, callerID, isTyping); err != nil { + return fmt.Errorf("set typing: %w", err) + } + + eventID := fmt.Sprintf("typing-%s-%s", convID, callerID) + resp := &iamv1.StreamChatEventsResponse{ + EventId: eventID, + Payload: &iamv1.StreamChatEventsResponse_Typing{ + Typing: &iamv1.TypingEvent{ + ConversationId: convID.String(), + UserId: callerID.String(), + UserName: callerName, + IsTyping: isTyping, + }, + }, + } + for _, part := range conv.Participants() { + if !part.IsActive() || part.UserID() == callerID { + continue + } + h.broadcaster.Publish(&chatinfra.Event{ + EventID: fmt.Sprintf("%s-%s", eventID, part.UserID()), + UserID: part.UserID(), + Response: resp, + }) + } + return nil +} diff --git a/services/iam/internal/application/chat/stream_handler.go b/services/iam/internal/application/chat/stream_handler.go new file mode 100644 index 00000000..44f1b31d --- /dev/null +++ b/services/iam/internal/application/chat/stream_handler.go @@ -0,0 +1,22 @@ +package chat + +import ( + "github.com/google/uuid" + + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" +) + +// StreamHandler subscribes a user to real-time chat events via the broadcaster. +type StreamHandler struct { + broadcaster *chatinfra.Broadcaster +} + +// NewStreamHandler constructs the handler. +func NewStreamHandler(broadcaster *chatinfra.Broadcaster) *StreamHandler { + return &StreamHandler{broadcaster: broadcaster} +} + +// Subscribe returns an event channel and unsub func for the given user. +func (h *StreamHandler) Subscribe(userID uuid.UUID) (<-chan *chatinfra.Event, func()) { + return h.broadcaster.Subscribe(userID) +} diff --git a/services/iam/internal/application/chat/update_group_handler.go b/services/iam/internal/application/chat/update_group_handler.go new file mode 100644 index 00000000..996bed52 --- /dev/null +++ b/services/iam/internal/application/chat/update_group_handler.go @@ -0,0 +1,40 @@ +package chat + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// UpdateGroupHandler updates a group conversation's name and avatar. +type UpdateGroupHandler struct { + convRepo chat.ConversationRepository +} + +// NewUpdateGroupHandler constructs the handler. +func NewUpdateGroupHandler(convRepo chat.ConversationRepository) *UpdateGroupHandler { + return &UpdateGroupHandler{convRepo: convRepo} +} + +// Handle updates the group conversation. Caller must be ADMIN or OWNER. +func (h *UpdateGroupHandler) Handle(ctx context.Context, callerID, convID uuid.UUID, name, avatarURL string) (*chat.Conversation, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, err + } + p := conv.FindParticipant(callerID) + if p == nil || !p.IsActive() { + return nil, fmt.Errorf("update group: %w", chat.ErrNotParticipant) + } + if !p.Role().IsAdminOrOwner() { + return nil, fmt.Errorf("update group: %w", chat.ErrNotAdmin) + } + conv.UpdateGroup(name, avatarURL) + if err := h.convRepo.UpdateGroup(ctx, conv); err != nil { + return nil, fmt.Errorf("update group: persist: %w", err) + } + return conv, nil +} diff --git a/services/iam/internal/application/chat/upload_attachment_handler.go b/services/iam/internal/application/chat/upload_attachment_handler.go new file mode 100644 index 00000000..ea231f02 --- /dev/null +++ b/services/iam/internal/application/chat/upload_attachment_handler.go @@ -0,0 +1,57 @@ +package chat + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/google/uuid" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + storageinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/storage" +) + +// UploadAttachmentHandler uploads a file and records an unlinked attachment. +type UploadAttachmentHandler struct { + convRepo chat.ConversationRepository + attRepo chat.AttachmentRepository + storage storageinfra.Service +} + +// NewUploadAttachmentHandler constructs the handler. +func NewUploadAttachmentHandler(convRepo chat.ConversationRepository, attRepo chat.AttachmentRepository, storage storageinfra.Service) *UploadAttachmentHandler { + return &UploadAttachmentHandler{convRepo: convRepo, attRepo: attRepo, storage: storage} +} + +// Handle validates participation, uploads the file, and persists an attachment +// that is not yet linked to any message. +func (h *UploadAttachmentHandler) Handle(ctx context.Context, uploaderID, convID uuid.UUID, fileName, contentType string, data []byte) (*chat.Attachment, error) { + conv, err := h.convRepo.GetByID(ctx, convID) + if err != nil { + return nil, err + } + p := conv.FindParticipant(uploaderID) + if p == nil || !p.IsActive() { + return nil, fmt.Errorf("upload attachment: %w", chat.ErrNotParticipant) + } + if h.storage == nil { + return nil, fmt.Errorf("upload attachment: %w", chat.ErrStorageUnavailable) + } + + fileURL, err := h.storage.UploadChatAttachment(ctx, convID.String(), fileName, bytes.NewReader(data), int64(len(data)), contentType) + if err != nil { + return nil, fmt.Errorf("upload attachment: store file: %w", err) + } + + thumbnailURL := "" + if strings.HasPrefix(contentType, "image/") { + thumbnailURL = fileURL + } + + att := chat.NewAttachment(convID, uploaderID, fileName, fileURL, contentType, int64(len(data)), thumbnailURL) + if err := h.attRepo.Create(ctx, att); err != nil { + return nil, fmt.Errorf("upload attachment: persist: %w", err) + } + return att, nil +} diff --git a/services/iam/internal/delivery/grpc/chat_handler.go b/services/iam/internal/delivery/grpc/chat_handler.go new file mode 100644 index 00000000..83be0d90 --- /dev/null +++ b/services/iam/internal/delivery/grpc/chat_handler.go @@ -0,0 +1,532 @@ +package grpc + +import ( + "context" + "errors" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + commonv1 "github.com/mutugading/goapps-backend/gen/common/v1" + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + appChat "github.com/mutugading/goapps-backend/services/iam/internal/application/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/postgres" +) + +// ChatHandler implements iamv1.ChatServiceServer. +type ChatHandler struct { + iamv1.UnimplementedChatServiceServer + createDirect *appChat.CreateDirectHandler + createGroup *appChat.CreateGroupHandler + getConv *appChat.GetConversationHandler + listConvs *appChat.ListConversationsHandler + leaveConv *appChat.LeaveConversationHandler + sendMsg *appChat.SendMessageHandler + editMsg *appChat.EditMessageHandler + deleteMsg *appChat.DeleteMessageHandler + listMsgs *appChat.ListMessagesHandler + markRead *appChat.MarkReadHandler + setTyping *appChat.SetTypingHandler + stream *appChat.StreamHandler + editHistory *appChat.GetEditHistoryHandler + clearHistory *appChat.ClearHistoryHandler + uploadAttach *appChat.UploadAttachmentHandler + attRepo chat.AttachmentRepository + userResolver *postgres.ChatUserResolver +} + +// NewChatHandler constructs the handler. +func NewChatHandler( + createDirect *appChat.CreateDirectHandler, + createGroup *appChat.CreateGroupHandler, + getConv *appChat.GetConversationHandler, + listConvs *appChat.ListConversationsHandler, + leaveConv *appChat.LeaveConversationHandler, + sendMsg *appChat.SendMessageHandler, + editMsg *appChat.EditMessageHandler, + deleteMsg *appChat.DeleteMessageHandler, + listMsgs *appChat.ListMessagesHandler, + markRead *appChat.MarkReadHandler, + setTyping *appChat.SetTypingHandler, + stream *appChat.StreamHandler, + editHistory *appChat.GetEditHistoryHandler, + clearHistory *appChat.ClearHistoryHandler, + uploadAttach *appChat.UploadAttachmentHandler, + attRepo chat.AttachmentRepository, + userResolver *postgres.ChatUserResolver, +) *ChatHandler { + return &ChatHandler{ + createDirect: createDirect, createGroup: createGroup, + getConv: getConv, listConvs: listConvs, leaveConv: leaveConv, + sendMsg: sendMsg, editMsg: editMsg, deleteMsg: deleteMsg, + listMsgs: listMsgs, markRead: markRead, setTyping: setTyping, + stream: stream, editHistory: editHistory, clearHistory: clearHistory, + uploadAttach: uploadAttach, attRepo: attRepo, + userResolver: userResolver, + } +} + +// CreateDirectConversation creates a 1:1 direct conversation. +func (h *ChatHandler) CreateDirectConversation(ctx context.Context, req *iamv1.CreateDirectConversationRequest) (*iamv1.CreateDirectConversationResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + peerID, err := uuid.Parse(req.GetPeerUserId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid peer_user_id: %v", err) + } + conv, err := h.createDirect.Handle(ctx, callerID, peerID) + if err != nil { + return nil, mapChatError(err) + } + return &iamv1.CreateDirectConversationResponse{Base: chatSuccessBase(), Data: h.convToProto(ctx, conv)}, nil +} + +// CreateGroupConversation creates a group conversation. +func (h *ChatHandler) CreateGroupConversation(ctx context.Context, req *iamv1.CreateGroupConversationRequest) (*iamv1.CreateGroupConversationResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + memberIDs := make([]uuid.UUID, 0, len(req.GetParticipantIds())) + for _, idStr := range req.GetParticipantIds() { + uid, parseErr := uuid.Parse(idStr) + if parseErr != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid participant_id: %v", parseErr) + } + memberIDs = append(memberIDs, uid) + } + conv, err := h.createGroup.Handle(ctx, callerID, req.GetName(), memberIDs) + if err != nil { + return nil, mapChatError(err) + } + return &iamv1.CreateGroupConversationResponse{Base: chatSuccessBase(), Data: h.convToProto(ctx, conv)}, nil +} + +// GetConversation returns a single conversation. +func (h *ChatHandler) GetConversation(ctx context.Context, req *iamv1.GetConversationRequest) (*iamv1.GetConversationResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + conv, err := h.getConv.Handle(ctx, callerID, convID) + if err != nil { + return nil, mapChatError(err) + } + return &iamv1.GetConversationResponse{Base: chatSuccessBase(), Data: h.convToProto(ctx, conv)}, nil +} + +// ListConversations returns paginated conversations. +func (h *ChatHandler) ListConversations(ctx context.Context, req *iamv1.ListConversationsRequest) (*iamv1.ListConversationsResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + page, pageSize := int(req.GetPage()), int(req.GetPageSize()) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + result, err := h.listConvs.Handle(ctx, callerID, page, pageSize) + if err != nil { + return nil, mapChatError(err) + } + protos := make([]*iamv1.ConversationProto, 0, len(result.Conversations)) + for _, summary := range result.Conversations { + protos = append(protos, h.summaryToProto(ctx, summary)) + } + totalPages := int32((result.Total + int64(pageSize) - 1) / int64(pageSize)) //nolint:gosec // bounded by page count + return &iamv1.ListConversationsResponse{ + Base: chatSuccessBase(), + Data: protos, + Pagination: &commonv1.PaginationResponse{ + CurrentPage: int32(page), //nolint:gosec // bounded by request validation + PageSize: int32(pageSize), //nolint:gosec // bounded by request validation + TotalItems: result.Total, + TotalPages: totalPages, + }, + }, nil +} + +// LeaveConversation removes the caller from a conversation. +func (h *ChatHandler) LeaveConversation(ctx context.Context, req *iamv1.LeaveConversationRequest) (*iamv1.LeaveConversationResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + if err := h.leaveConv.Handle(ctx, callerID, convID); err != nil { + return nil, mapChatError(err) + } + return &iamv1.LeaveConversationResponse{Base: chatSuccessBase()}, nil +} + +// SendMessage sends a message to a conversation. +func (h *ChatHandler) SendMessage(ctx context.Context, req *iamv1.SendMessageRequest) (*iamv1.SendMessageResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + replyToID := uuid.Nil + if req.GetReplyToId() != "" { + replyToID, err = uuid.Parse(req.GetReplyToId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid reply_to_id: %v", err) + } + } + attachmentIDs := make([]uuid.UUID, 0, len(req.GetAttachmentIds())) + for _, idStr := range req.GetAttachmentIds() { + aid, parseErr := uuid.Parse(idStr) + if parseErr != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment_id: %v", parseErr) + } + attachmentIDs = append(attachmentIDs, aid) + } + msg, attProtos, err := h.sendMsg.Handle(ctx, callerID, convID, req.GetBody(), replyToID, attachmentIDs) + if err != nil { + return nil, mapChatError(err) + } + mp := msgToProto(msg, req.GetBody(), nil) + mp.Attachments = attProtos + return &iamv1.SendMessageResponse{Base: chatSuccessBase(), Data: mp}, nil +} + +// EditMessage edits an existing message. +func (h *ChatHandler) EditMessage(ctx context.Context, req *iamv1.EditMessageRequest) (*iamv1.EditMessageResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + msgID, err := uuid.Parse(req.GetMessageId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid message_id: %v", err) + } + msg, err := h.editMsg.Handle(ctx, callerID, convID, msgID, req.GetBody()) + if err != nil { + return nil, mapChatError(err) + } + return &iamv1.EditMessageResponse{Base: chatSuccessBase(), Data: msgToProto(msg, req.GetBody(), nil)}, nil +} + +// DeleteMessage soft-deletes a message. +func (h *ChatHandler) DeleteMessage(ctx context.Context, req *iamv1.DeleteMessageRequest) (*iamv1.DeleteMessageResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + msgID, err := uuid.Parse(req.GetMessageId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid message_id: %v", err) + } + if err := h.deleteMsg.Handle(ctx, callerID, convID, msgID); err != nil { + return nil, mapChatError(err) + } + return &iamv1.DeleteMessageResponse{Base: chatSuccessBase()}, nil +} + +// ListMessages returns paginated messages for a conversation. +func (h *ChatHandler) ListMessages(ctx context.Context, req *iamv1.ListMessagesRequest) (*iamv1.ListMessagesResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + pageSize := int(req.GetPageSize()) + if pageSize < 1 { + pageSize = 30 + } + result, err := h.listMsgs.Handle(ctx, callerID, convID, pageSize, req.GetBeforeCursor()) + if err != nil { + return nil, mapChatError(err) + } + senderIDs := make([]uuid.UUID, 0, len(result.Messages)) + for _, dm := range result.Messages { + senderIDs = append(senderIDs, dm.SenderUserID()) + } + senderMap, _ := h.userResolver.ResolveUsers(ctx, senderIDs) //nolint:errcheck // name enrichment is best-effort + + msgIDs := make([]uuid.UUID, 0, len(result.Messages)) + for _, dm := range result.Messages { + msgIDs = append(msgIDs, dm.MessageID()) + } + attByMsg := map[uuid.UUID][]*chat.Attachment{} + if h.attRepo != nil { + if loaded, attErr := h.attRepo.ListByMessageIDs(ctx, msgIDs); attErr != nil { + log.Warn().Err(attErr).Msg("chat handler: load attachments for list messages") + } else { + attByMsg = loaded + } + } + + protos := make([]*iamv1.MessageProto, 0, len(result.Messages)) + for _, dm := range result.Messages { + mp := msgToProto(dm.Message, dm.PlainBody, dm.ReadReceipts()) + if info := senderMap[dm.SenderUserID()]; info != nil { + if info.FullName != "" { + mp.SenderName = info.FullName + } else { + mp.SenderName = info.Username + } + } + for _, att := range attByMsg[dm.MessageID()] { + mp.Attachments = append(mp.Attachments, attachmentToProto(att)) + } + protos = append(protos, mp) + } + return &iamv1.ListMessagesResponse{ + Base: chatSuccessBase(), + Data: protos, + NextCursor: result.NextCursor, + HasMore: result.HasMore, + }, nil +} + +// GetMessageEditHistory returns the edit history for a specific message. +func (h *ChatHandler) GetMessageEditHistory(ctx context.Context, req *iamv1.GetMessageEditHistoryRequest) (*iamv1.GetMessageEditHistoryResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + msgID, err := uuid.Parse(req.GetMessageId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid message_id: %v", err) + } + entries, err := h.editHistory.Handle(ctx, callerID, convID, msgID) + if err != nil { + return nil, mapChatError(err) + } + protos := make([]*iamv1.EditHistoryEntryProto, 0, len(entries)) + for _, e := range entries { + protos = append(protos, &iamv1.EditHistoryEntryProto{ + HistoryId: e.HistoryID(), + Body: e.PlainBody, + EditedBy: e.EditedBy().String(), + EditedAt: e.EditedAt().UTC().Format(time.RFC3339Nano), + }) + } + return &iamv1.GetMessageEditHistoryResponse{Base: chatSuccessBase(), Data: protos}, nil +} + +// MarkConversationRead marks all messages as read. +func (h *ChatHandler) MarkConversationRead(ctx context.Context, req *iamv1.MarkConversationReadRequest) (*iamv1.MarkConversationReadResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + if err := h.markRead.Handle(ctx, callerID, convID); err != nil { + return nil, mapChatError(err) + } + return &iamv1.MarkConversationReadResponse{Base: chatSuccessBase()}, nil +} + +// ClearConversationHistory clears the calling user's own view of a +// conversation's message history. Other participants are unaffected. +func (h *ChatHandler) ClearConversationHistory(ctx context.Context, req *iamv1.ClearConversationHistoryRequest) (*iamv1.ClearConversationHistoryResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + if err := h.clearHistory.Handle(ctx, callerID, convID); err != nil { + return nil, mapChatError(err) + } + return &iamv1.ClearConversationHistoryResponse{Base: chatSuccessBase()}, nil +} + +// UploadChatAttachment uploads a file to a conversation, returning an +// attachment that can later be linked to a message via SendMessage. +func (h *ChatHandler) UploadChatAttachment(ctx context.Context, req *iamv1.UploadChatAttachmentRequest) (*iamv1.UploadChatAttachmentResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + att, err := h.uploadAttach.Handle(ctx, callerID, convID, req.GetFileName(), req.GetContentType(), req.GetFileData()) + if err != nil { + return nil, mapChatError(err) + } + return &iamv1.UploadChatAttachmentResponse{Base: chatSuccessBase(), Data: attachmentToProto(att)}, nil +} + +// SetTyping sets the typing indicator. +func (h *ChatHandler) SetTyping(ctx context.Context, req *iamv1.SetTypingRequest) (*iamv1.SetTypingResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + convID, err := uuid.Parse(req.GetConversationId()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid conversation_id: %v", err) + } + if err := h.setTyping.Handle(ctx, callerID, convID, req.GetIsTyping(), ""); err != nil { + return nil, mapChatError(err) + } + return &iamv1.SetTypingResponse{Base: chatSuccessBase()}, nil +} + +// StreamChatEvents opens a server-streaming connection for chat events. +func (h *ChatHandler) StreamChatEvents(_ *iamv1.StreamChatEventsRequest, stream iamv1.ChatService_StreamChatEventsServer) error { + ctx := stream.Context() + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return err + } + ch, unsub := h.stream.Subscribe(callerID) + defer unsub() + for { + select { + case <-ctx.Done(): + return nil + case evt, ok := <-ch: + if !ok { + return nil + } + if sendErr := stream.Send(evt.Response); sendErr != nil { + return sendErr + } + } + } +} + +// summaryToProto builds a ConversationProto and populates LastMessage + +// UnreadCount from the batch-loaded summary (see ListConversationsHandler). +func (h *ChatHandler) summaryToProto(ctx context.Context, summary *appChat.ConversationSummary) *iamv1.ConversationProto { + proto := h.convToProto(ctx, summary.Conversation) + proto.UnreadCount = summary.UnreadCount + if summary.LastMessage != nil { + proto.LastMessage = msgToProto(summary.LastMessage, summary.LastMessageBody, nil) + } + return proto +} + +func (h *ChatHandler) convToProto(ctx context.Context, conv *chat.Conversation) *iamv1.ConversationProto { + userIDs := make([]uuid.UUID, 0, len(conv.Participants())) + for _, p := range conv.Participants() { + userIDs = append(userIDs, p.UserID()) + } + userMap, _ := h.userResolver.ResolveUsers(ctx, userIDs) //nolint:errcheck // name enrichment is best-effort + + parts := make([]*iamv1.ParticipantProto, 0, len(conv.Participants())) + for _, p := range conv.Participants() { + pp := &iamv1.ParticipantProto{ + UserId: p.UserID().String(), + Role: p.Role().String(), + JoinedAt: p.JoinedAt().UTC().Format(time.RFC3339Nano), + } + if info := userMap[p.UserID()]; info != nil { + pp.Username = info.Username + pp.FullName = info.FullName + pp.AvatarUrl = info.AvatarURL + } + parts = append(parts, pp) + } + return &iamv1.ConversationProto{ + ConversationId: conv.ID().String(), + Type: conv.Type().String(), + Name: conv.Name(), + AvatarUrl: conv.AvatarURL(), + Participants: parts, + CreatedAt: conv.CreatedAt().UTC().Format(time.RFC3339Nano), + UpdatedAt: conv.UpdatedAt().UTC().Format(time.RFC3339Nano), + } +} + +func msgToProto(msg *chat.Message, plainBody string, receipts []*chat.ReadReceipt) *iamv1.MessageProto { + receiptProtos := make([]*iamv1.ReadReceiptProto, 0, len(receipts)) + for _, r := range receipts { + receiptProtos = append(receiptProtos, &iamv1.ReadReceiptProto{ + UserId: r.UserID().String(), + ReadAt: r.ReadAt().UTC().Format(time.RFC3339Nano), + }) + } + replyTo := "" + if msg.ReplyToID() != uuid.Nil { + replyTo = msg.ReplyToID().String() + } + return &iamv1.MessageProto{ + MessageId: msg.MessageID().String(), + ConversationId: msg.ConversationID().String(), + SenderUserId: msg.SenderUserID().String(), + Body: plainBody, + IsEdited: msg.IsEdited(), + IsDeleted: msg.IsDeleted(), + ReplyToId: replyTo, + ReadReceipts: receiptProtos, + CreatedAt: msg.CreatedAt().UTC().Format(time.RFC3339Nano), + UpdatedAt: msg.UpdatedAt().UTC().Format(time.RFC3339Nano), + } +} + +func attachmentToProto(a *chat.Attachment) *iamv1.AttachmentProto { + return &iamv1.AttachmentProto{ + AttachmentId: a.AttachmentID().String(), + FileName: a.FileName(), + FileUrl: a.FileURL(), + ContentType: a.ContentType(), + FileSize: a.FileSize(), + ThumbnailUrl: a.ThumbnailURL(), + } +} + +func chatSuccessBase() *commonv1.BaseResponse { + return &commonv1.BaseResponse{IsSuccess: true, Message: "success", StatusCode: "200"} +} + +func mapChatError(err error) error { + switch { + case errors.Is(err, chat.ErrConversationNotFound), errors.Is(err, chat.ErrMessageNotFound), errors.Is(err, chat.ErrAttachmentNotFound): + return status.Errorf(codes.NotFound, "%v", err) + case errors.Is(err, chat.ErrStorageUnavailable): + return status.Errorf(codes.Unavailable, "%v", err) + case errors.Is(err, chat.ErrNotParticipant), errors.Is(err, chat.ErrNotAuthor), errors.Is(err, chat.ErrNotAdmin): + return status.Errorf(codes.PermissionDenied, "%v", err) + case errors.Is(err, chat.ErrDirectConversationFull), errors.Is(err, chat.ErrAlreadyParticipant): + return status.Errorf(codes.FailedPrecondition, "%v", err) + default: + log.Error().Err(err).Msg("chat handler: unhandled error") + return status.Errorf(codes.Internal, "internal error") + } +} diff --git a/services/iam/internal/delivery/grpc/permission_interceptor.go b/services/iam/internal/delivery/grpc/permission_interceptor.go index 17fa3b3a..267204c7 100644 --- a/services/iam/internal/delivery/grpc/permission_interceptor.go +++ b/services/iam/internal/delivery/grpc/permission_interceptor.go @@ -217,6 +217,30 @@ var methodPermissions = map[string]PermissionRequirement{ // RequestNotification is for internal service-to-service use only; // auth is enforced via x-internal-token bypass in the auth interceptor. "/iam.v1.NotificationService/RequestNotification": {Permission: ""}, + + // Chat Service — authenticated only; participant checks are at handler level. + "/iam.v1.ChatService/CreateDirectConversation": {Permission: ""}, + "/iam.v1.ChatService/CreateGroupConversation": {Permission: ""}, + "/iam.v1.ChatService/GetConversation": {Permission: ""}, + "/iam.v1.ChatService/ListConversations": {Permission: ""}, + "/iam.v1.ChatService/UpdateGroupConversation": {Permission: ""}, + "/iam.v1.ChatService/AddParticipants": {Permission: ""}, + "/iam.v1.ChatService/RemoveParticipant": {Permission: ""}, + "/iam.v1.ChatService/LeaveConversation": {Permission: ""}, + "/iam.v1.ChatService/SendMessage": {Permission: ""}, + "/iam.v1.ChatService/EditMessage": {Permission: ""}, + "/iam.v1.ChatService/DeleteMessage": {Permission: ""}, + "/iam.v1.ChatService/ListMessages": {Permission: ""}, + "/iam.v1.ChatService/StreamChatEvents": {Permission: ""}, + "/iam.v1.ChatService/MarkConversationRead": {Permission: ""}, + "/iam.v1.ChatService/SetTyping": {Permission: ""}, + "/iam.v1.ChatService/GetMessageEditHistory": {Permission: ""}, + "/iam.v1.ChatService/ClearConversationHistory": {Permission: ""}, + "/iam.v1.ChatService/UploadChatAttachment": {Permission: ""}, + + // Presence Service — authenticated only. + "/iam.v1.PresenceService/Heartbeat": {Permission: ""}, + "/iam.v1.PresenceService/GetOnlineUsers": {Permission: ""}, } // PermissionInterceptor creates a unary interceptor that checks if the diff --git a/services/iam/internal/delivery/grpc/presence_handler.go b/services/iam/internal/delivery/grpc/presence_handler.go new file mode 100644 index 00000000..3606b4dd --- /dev/null +++ b/services/iam/internal/delivery/grpc/presence_handler.go @@ -0,0 +1,62 @@ +package grpc + +import ( + "context" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + commonv1 "github.com/mutugading/goapps-backend/gen/common/v1" + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" +) + +// PresenceHandler implements iamv1.PresenceServiceServer. +type PresenceHandler struct { + iamv1.UnimplementedPresenceServiceServer + presence *chatinfra.PresenceService +} + +// NewPresenceHandler constructs the handler. +func NewPresenceHandler(presence *chatinfra.PresenceService) *PresenceHandler { + return &PresenceHandler{presence: presence} +} + +// Heartbeat updates the caller's online presence. +func (h *PresenceHandler) Heartbeat(ctx context.Context, _ *iamv1.HeartbeatRequest) (*iamv1.HeartbeatResponse, error) { + callerID, err := getUserIDFromContext(ctx) + if err != nil { + return nil, err + } + if err := h.presence.SetOnline(ctx, callerID); err != nil { + return nil, status.Errorf(codes.Internal, "heartbeat failed: %v", err) + } + return &iamv1.HeartbeatResponse{ + Base: &commonv1.BaseResponse{IsSuccess: true, Message: "success", StatusCode: "200"}, + }, nil +} + +// GetOnlineUsers returns which users are currently online. +func (h *PresenceHandler) GetOnlineUsers(ctx context.Context, req *iamv1.GetOnlineUsersRequest) (*iamv1.GetOnlineUsersResponse, error) { + userIDs := make([]uuid.UUID, 0, len(req.GetUserIds())) + for _, idStr := range req.GetUserIds() { + uid, err := uuid.Parse(idStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user_id %q: %v", idStr, err) + } + userIDs = append(userIDs, uid) + } + onlineIDs, err := h.presence.GetOnlineUsers(ctx, userIDs) + if err != nil { + return nil, status.Errorf(codes.Internal, "get online users: %v", err) + } + strs := make([]string, len(onlineIDs)) + for i, uid := range onlineIDs { + strs[i] = uid.String() + } + return &iamv1.GetOnlineUsersResponse{ + Base: &commonv1.BaseResponse{IsSuccess: true, Message: "success", StatusCode: "200"}, + UserIds: strs, + }, nil +} diff --git a/services/iam/internal/domain/chat/attachment.go b/services/iam/internal/domain/chat/attachment.go new file mode 100644 index 00000000..15a84b5f --- /dev/null +++ b/services/iam/internal/domain/chat/attachment.go @@ -0,0 +1,93 @@ +// Package chat contains the domain model for the chat feature: conversations, +// messages, read receipts, and attachments. +package chat + +import ( + "time" + + "github.com/google/uuid" +) + +// Attachment is the aggregate root for a chat file or image attachment. +type Attachment struct { + attachmentID uuid.UUID + conversationID uuid.UUID + messageID *uuid.UUID + uploaderUserID uuid.UUID + fileName string + fileURL string + contentType string + fileSize int64 + thumbnailURL string + createdAt time.Time +} + +// NewAttachment creates a new attachment not yet linked to a message. +func NewAttachment(convID, uploaderID uuid.UUID, fileName, fileURL, contentType string, fileSize int64, thumbnailURL string) *Attachment { + return &Attachment{ + attachmentID: uuid.New(), + conversationID: convID, + messageID: nil, + uploaderUserID: uploaderID, + fileName: fileName, + fileURL: fileURL, + contentType: contentType, + fileSize: fileSize, + thumbnailURL: thumbnailURL, + createdAt: time.Now().UTC(), + } +} + +// ReconstructAttachment rebuilds an Attachment from persistence. +func ReconstructAttachment( + attachmentID, conversationID uuid.UUID, messageID *uuid.UUID, uploaderUserID uuid.UUID, + fileName, fileURL, contentType string, fileSize int64, thumbnailURL string, createdAt time.Time, +) *Attachment { + return &Attachment{ + attachmentID: attachmentID, + conversationID: conversationID, + messageID: messageID, + uploaderUserID: uploaderUserID, + fileName: fileName, + fileURL: fileURL, + contentType: contentType, + fileSize: fileSize, + thumbnailURL: thumbnailURL, + createdAt: createdAt, + } +} + +// AttachmentID returns the attachment ID. +func (a *Attachment) AttachmentID() uuid.UUID { return a.attachmentID } + +// ConversationID returns the conversation ID this attachment belongs to. +func (a *Attachment) ConversationID() uuid.UUID { return a.conversationID } + +// MessageID returns the linked message ID, or nil if not yet linked. +func (a *Attachment) MessageID() *uuid.UUID { return a.messageID } + +// UploaderUserID returns the ID of the user who uploaded the attachment. +func (a *Attachment) UploaderUserID() uuid.UUID { return a.uploaderUserID } + +// FileName returns the original file name. +func (a *Attachment) FileName() string { return a.fileName } + +// FileURL returns the stored file URL. +func (a *Attachment) FileURL() string { return a.fileURL } + +// ContentType returns the MIME content type. +func (a *Attachment) ContentType() string { return a.contentType } + +// FileSize returns the file size in bytes. +func (a *Attachment) FileSize() int64 { return a.fileSize } + +// ThumbnailURL returns the thumbnail URL, or an empty string if none. +func (a *Attachment) ThumbnailURL() string { return a.thumbnailURL } + +// CreatedAt returns the creation timestamp. +func (a *Attachment) CreatedAt() time.Time { return a.createdAt } + +// LinkToMessage links the attachment to a persisted message. +func (a *Attachment) LinkToMessage(msgID uuid.UUID) { + a.messageID = &msgID +} diff --git a/services/iam/internal/domain/chat/attachment_test.go b/services/iam/internal/domain/chat/attachment_test.go new file mode 100644 index 00000000..50ee9b70 --- /dev/null +++ b/services/iam/internal/domain/chat/attachment_test.go @@ -0,0 +1,88 @@ +package chat + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewAttachment(t *testing.T) { + convID := uuid.New() + uploaderID := uuid.New() + + tests := []struct { + name string + fileName string + fileURL string + contentType string + fileSize int64 + thumbnailURL string + }{ + { + name: "image with thumbnail", + fileName: "photo.png", + fileURL: "https://cdn/iam/chat/attachments/x/y.png", + contentType: "image/png", + fileSize: 2048, + thumbnailURL: "https://cdn/iam/chat/attachments/x/y.png", + }, + { + name: "document without thumbnail", + fileName: "report.pdf", + fileURL: "https://cdn/iam/chat/attachments/x/z.pdf", + contentType: "application/pdf", + fileSize: 4096, + thumbnailURL: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := NewAttachment(convID, uploaderID, tt.fileName, tt.fileURL, tt.contentType, tt.fileSize, tt.thumbnailURL) + require.NotNil(t, a) + + assert.NotEqual(t, uuid.Nil, a.AttachmentID()) + assert.Equal(t, convID, a.ConversationID()) + assert.Equal(t, uploaderID, a.UploaderUserID()) + assert.Equal(t, tt.fileName, a.FileName()) + assert.Equal(t, tt.fileURL, a.FileURL()) + assert.Equal(t, tt.contentType, a.ContentType()) + assert.Equal(t, tt.fileSize, a.FileSize()) + assert.Equal(t, tt.thumbnailURL, a.ThumbnailURL()) + assert.Nil(t, a.MessageID(), "new attachment must be unlinked") + assert.False(t, a.CreatedAt().IsZero()) + }) + } +} + +func TestAttachment_LinkToMessage(t *testing.T) { + a := NewAttachment(uuid.New(), uuid.New(), "f.txt", "url", "text/plain", 10, "") + require.Nil(t, a.MessageID()) + + msgID := uuid.New() + a.LinkToMessage(msgID) + + require.NotNil(t, a.MessageID()) + assert.Equal(t, msgID, *a.MessageID()) +} + +func TestReconstructAttachment(t *testing.T) { + attID := uuid.New() + convID := uuid.New() + msgID := uuid.New() + uploaderID := uuid.New() + a := NewAttachment(convID, uploaderID, "f.txt", "url", "text/plain", 10, "") + createdAt := a.CreatedAt() + + got := ReconstructAttachment(attID, convID, &msgID, uploaderID, "f.txt", "url", "text/plain", 10, "thumb", createdAt) + + assert.Equal(t, attID, got.AttachmentID()) + assert.Equal(t, convID, got.ConversationID()) + require.NotNil(t, got.MessageID()) + assert.Equal(t, msgID, *got.MessageID()) + assert.Equal(t, uploaderID, got.UploaderUserID()) + assert.Equal(t, "thumb", got.ThumbnailURL()) + assert.Equal(t, createdAt, got.CreatedAt()) +} diff --git a/services/iam/internal/domain/chat/entity.go b/services/iam/internal/domain/chat/entity.go new file mode 100644 index 00000000..1c79db6e --- /dev/null +++ b/services/iam/internal/domain/chat/entity.go @@ -0,0 +1,344 @@ +package chat + +import ( + "time" + + "github.com/google/uuid" +) + +// Participant is a member of a conversation. +type Participant struct { + conversationID uuid.UUID + userID uuid.UUID + role Role + joinedAt time.Time + leftAt *time.Time + lastReadAt *time.Time + historyClearedAt *time.Time +} + +// ReconstructParticipant rebuilds a Participant from persistence. +func ReconstructParticipant(conversationID, userID uuid.UUID, role Role, joinedAt time.Time, leftAt, lastReadAt, historyClearedAt *time.Time) *Participant { + return &Participant{ + conversationID: conversationID, userID: userID, role: role, + joinedAt: joinedAt, leftAt: leftAt, lastReadAt: lastReadAt, + historyClearedAt: historyClearedAt, + } +} + +// UserID returns the participant user ID. +func (p *Participant) UserID() uuid.UUID { return p.userID } + +// Role returns the participant role. +func (p *Participant) Role() Role { return p.role } + +// JoinedAt returns when the participant joined. +func (p *Participant) JoinedAt() time.Time { return p.joinedAt } + +// LeftAt returns when the participant left, or nil. +func (p *Participant) LeftAt() *time.Time { return p.leftAt } + +// LastReadAt returns the last read timestamp, or nil. +func (p *Participant) LastReadAt() *time.Time { return p.lastReadAt } + +// HistoryClearedAt returns the timestamp at which this participant last +// cleared their own view of the conversation history, or nil if never cleared. +func (p *Participant) HistoryClearedAt() *time.Time { return p.historyClearedAt } + +// IsActive returns true if the participant has not left. +func (p *Participant) IsActive() bool { return p.leftAt == nil } + +// Conversation is the aggregate root for a chat conversation. +type Conversation struct { + id uuid.UUID + convType Type + name string + avatarURL string + encryptionKey []byte + convKeyPlain []byte + participants []*Participant + createdBy string + createdAt time.Time + updatedAt time.Time + deletedAt *time.Time +} + +// NewDirectConversation creates a 1:1 conversation. +func NewDirectConversation(ownerID, peerID uuid.UUID, convKeyPlain, encryptionKey []byte) (*Conversation, error) { + now := time.Now().UTC() + conv := &Conversation{ + id: uuid.New(), + convType: TypeDirect, + encryptionKey: encryptionKey, + convKeyPlain: convKeyPlain, + createdBy: ownerID.String(), + createdAt: now, + updatedAt: now, + } + conv.participants = []*Participant{ + {conversationID: conv.id, userID: ownerID, role: RoleOwner, joinedAt: now}, + {conversationID: conv.id, userID: peerID, role: RoleMember, joinedAt: now}, + } + return conv, nil +} + +// NewGroupConversation creates a group conversation. +func NewGroupConversation(ownerID uuid.UUID, name string, memberIDs []uuid.UUID, convKeyPlain, encryptionKey []byte) (*Conversation, error) { + now := time.Now().UTC() + conv := &Conversation{ + id: uuid.New(), + convType: TypeGroup, + name: name, + encryptionKey: encryptionKey, + convKeyPlain: convKeyPlain, + createdBy: ownerID.String(), + createdAt: now, + updatedAt: now, + } + conv.participants = append(conv.participants, + &Participant{conversationID: conv.id, userID: ownerID, role: RoleOwner, joinedAt: now}, + ) + for _, mid := range memberIDs { + conv.participants = append(conv.participants, + &Participant{conversationID: conv.id, userID: mid, role: RoleMember, joinedAt: now}, + ) + } + return conv, nil +} + +// Reconstruct rebuilds a Conversation from persistence. +func Reconstruct( + id uuid.UUID, convType Type, name, avatarURL string, + encryptionKey []byte, createdBy string, + createdAt, updatedAt time.Time, deletedAt *time.Time, + participants []*Participant, +) *Conversation { + return &Conversation{ + id: id, convType: convType, name: name, avatarURL: avatarURL, + encryptionKey: encryptionKey, createdBy: createdBy, + createdAt: createdAt, updatedAt: updatedAt, deletedAt: deletedAt, + participants: participants, + } +} + +// ID returns the conversation ID. +func (c *Conversation) ID() uuid.UUID { return c.id } + +// Type returns the conversation type. +func (c *Conversation) Type() Type { return c.convType } + +// Name returns the conversation name. +func (c *Conversation) Name() string { return c.name } + +// AvatarURL returns the conversation avatar URL. +func (c *Conversation) AvatarURL() string { return c.avatarURL } + +// EncryptionKey returns the encrypted conversation key bytes. +func (c *Conversation) EncryptionKey() []byte { return c.encryptionKey } + +// ConvKeyPlain returns the decrypted conversation key. +func (c *Conversation) ConvKeyPlain() []byte { return c.convKeyPlain } + +// CreatedBy returns the creator identifier. +func (c *Conversation) CreatedBy() string { return c.createdBy } + +// CreatedAt returns the creation timestamp. +func (c *Conversation) CreatedAt() time.Time { return c.createdAt } + +// UpdatedAt returns the last update timestamp. +func (c *Conversation) UpdatedAt() time.Time { return c.updatedAt } + +// DeletedAt returns the deletion timestamp, or nil. +func (c *Conversation) DeletedAt() *time.Time { return c.deletedAt } + +// Participants returns all participants. +func (c *Conversation) Participants() []*Participant { return c.participants } + +// SetConvKeyPlain sets the decrypted conversation key. +func (c *Conversation) SetConvKeyPlain(key []byte) { c.convKeyPlain = key } + +// ActiveParticipantCount returns the number of non-left participants. +func (c *Conversation) ActiveParticipantCount() int { + count := 0 + for _, p := range c.participants { + if p.IsActive() { + count++ + } + } + return count +} + +// FindParticipant returns the participant with the given userID, or nil. +func (c *Conversation) FindParticipant(userID uuid.UUID) *Participant { + for _, p := range c.participants { + if p.userID == userID { + return p + } + } + return nil +} + +// AddParticipant adds a new active participant. +func (c *Conversation) AddParticipant(userID uuid.UUID, role Role) error { + if c.convType == TypeDirect && c.ActiveParticipantCount() >= 2 { + return ErrDirectConversationFull + } + for _, p := range c.participants { + if p.userID == userID && p.IsActive() { + return ErrAlreadyParticipant + } + } + now := time.Now().UTC() + c.participants = append(c.participants, &Participant{ + conversationID: c.id, userID: userID, role: role, joinedAt: now, + }) + c.updatedAt = now + return nil +} + +// UpdateGroup updates mutable group fields. +func (c *Conversation) UpdateGroup(name, avatarURL string) { + c.name = name + c.avatarURL = avatarURL + c.updatedAt = time.Now().UTC() +} + +// EditHistoryEntry is an immutable snapshot of a message before an edit. +type EditHistoryEntry struct { + historyID int64 + messageID uuid.UUID + bodyEncrypted []byte + editedBy uuid.UUID + editedAt time.Time +} + +// ReconstructEditHistory rebuilds an edit history entry from persistence. +func ReconstructEditHistory(historyID int64, messageID uuid.UUID, bodyEncrypted []byte, editedBy uuid.UUID, editedAt time.Time) *EditHistoryEntry { + return &EditHistoryEntry{historyID: historyID, messageID: messageID, bodyEncrypted: bodyEncrypted, editedBy: editedBy, editedAt: editedAt} +} + +// HistoryID returns the history entry ID. +func (e *EditHistoryEntry) HistoryID() int64 { return e.historyID } + +// MessageID returns the message this entry belongs to. +func (e *EditHistoryEntry) MessageID() uuid.UUID { return e.messageID } + +// BodyEncrypted returns the encrypted pre-edit body. +func (e *EditHistoryEntry) BodyEncrypted() []byte { return e.bodyEncrypted } + +// EditedBy returns the user who made the edit. +func (e *EditHistoryEntry) EditedBy() uuid.UUID { return e.editedBy } + +// EditedAt returns when the edit was made. +func (e *EditHistoryEntry) EditedAt() time.Time { return e.editedAt } + +// ReadReceipt records that a user has read a message. +type ReadReceipt struct { + messageID uuid.UUID + userID uuid.UUID + readAt time.Time +} + +// MessageID returns the message ID. +func (r *ReadReceipt) MessageID() uuid.UUID { return r.messageID } + +// UserID returns the user ID. +func (r *ReadReceipt) UserID() uuid.UUID { return r.userID } + +// ReadAt returns the read timestamp. +func (r *ReadReceipt) ReadAt() time.Time { return r.readAt } + +// NewReadReceipt constructs a ReadReceipt from persistence data. +func NewReadReceipt(messageID, userID uuid.UUID, readAt time.Time) *ReadReceipt { + return &ReadReceipt{messageID: messageID, userID: userID, readAt: readAt} +} + +// Message is the aggregate root for a chat message. +type Message struct { + messageID uuid.UUID + conversationID uuid.UUID + senderUserID uuid.UUID + bodyEncrypted []byte + bodyPlainEncrypted []byte + isEdited bool + isDeleted bool + replyToID uuid.UUID + readReceipts []*ReadReceipt + createdAt time.Time + updatedAt time.Time +} + +// NewMessage creates a new message with encrypted bodies. +func NewMessage(conversationID, senderID uuid.UUID, bodyEncrypted, bodyPlainEncrypted []byte, replyToID uuid.UUID) *Message { + now := time.Now().UTC() + return &Message{ + messageID: uuid.New(), conversationID: conversationID, senderUserID: senderID, + bodyEncrypted: bodyEncrypted, bodyPlainEncrypted: bodyPlainEncrypted, + replyToID: replyToID, createdAt: now, updatedAt: now, + } +} + +// ReconstructMessage rebuilds a Message from persistence. +func ReconstructMessage( + messageID, conversationID, senderID uuid.UUID, + bodyEncrypted, bodyPlainEncrypted []byte, + isEdited, isDeleted bool, replyToID uuid.UUID, + receipts []*ReadReceipt, + createdAt, updatedAt time.Time, +) *Message { + return &Message{ + messageID: messageID, conversationID: conversationID, senderUserID: senderID, + bodyEncrypted: bodyEncrypted, bodyPlainEncrypted: bodyPlainEncrypted, + isEdited: isEdited, isDeleted: isDeleted, replyToID: replyToID, + readReceipts: receipts, createdAt: createdAt, updatedAt: updatedAt, + } +} + +// MessageID returns the message ID. +func (m *Message) MessageID() uuid.UUID { return m.messageID } + +// ConversationID returns the conversation ID. +func (m *Message) ConversationID() uuid.UUID { return m.conversationID } + +// SenderUserID returns the sender user ID. +func (m *Message) SenderUserID() uuid.UUID { return m.senderUserID } + +// BodyEncrypted returns the encrypted message body. +func (m *Message) BodyEncrypted() []byte { return m.bodyEncrypted } + +// BodyPlainEncrypted returns the encrypted plaintext body. +func (m *Message) BodyPlainEncrypted() []byte { return m.bodyPlainEncrypted } + +// IsEdited returns true if the message has been edited. +func (m *Message) IsEdited() bool { return m.isEdited } + +// IsDeleted returns true if the message has been soft-deleted. +func (m *Message) IsDeleted() bool { return m.isDeleted } + +// ReplyToID returns the reply-to message ID, or uuid.Nil if none. +func (m *Message) ReplyToID() uuid.UUID { return m.replyToID } + +// ReadReceipts returns all read receipts for this message. +func (m *Message) ReadReceipts() []*ReadReceipt { return m.readReceipts } + +// CreatedAt returns the creation timestamp. +func (m *Message) CreatedAt() time.Time { return m.createdAt } + +// UpdatedAt returns the last update timestamp. +func (m *Message) UpdatedAt() time.Time { return m.updatedAt } + +// Edit replaces the body and returns the OLD bodyEncrypted for history. +func (m *Message) Edit(newBodyEncrypted, newBodyPlainEncrypted []byte) []byte { + old := m.bodyEncrypted + m.bodyEncrypted = newBodyEncrypted + m.bodyPlainEncrypted = newBodyPlainEncrypted + m.isEdited = true + m.updatedAt = time.Now().UTC() + return old +} + +// SoftDelete marks the message as deleted. +func (m *Message) SoftDelete() { + m.isDeleted = true + m.updatedAt = time.Now().UTC() +} diff --git a/services/iam/internal/domain/chat/entity_test.go b/services/iam/internal/domain/chat/entity_test.go new file mode 100644 index 00000000..4b38e0be --- /dev/null +++ b/services/iam/internal/domain/chat/entity_test.go @@ -0,0 +1,62 @@ +package chat_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +func TestConversation_NewDirect(t *testing.T) { + ownerID := uuid.New() + peerID := uuid.New() + convKey := make([]byte, 32) + encKey := []byte("encrypted-key-bytes") + + conv, err := chat.NewDirectConversation(ownerID, peerID, convKey, encKey) + require.NoError(t, err) + assert.Equal(t, chat.TypeDirect, conv.Type()) + assert.Len(t, conv.Participants(), 2) + assert.NotEqual(t, uuid.Nil, conv.ID()) +} + +func TestConversation_DirectParticipantLimit(t *testing.T) { + ownerID := uuid.New() + peerID := uuid.New() + conv, err := chat.NewDirectConversation(ownerID, peerID, make([]byte, 32), []byte("enc")) + require.NoError(t, err) + + err = conv.AddParticipant(uuid.New(), chat.RoleMember) + assert.ErrorIs(t, err, chat.ErrDirectConversationFull) +} + +func TestMessage_SoftDelete(t *testing.T) { + msg := chat.ReconstructMessage( + uuid.New(), uuid.New(), uuid.New(), + []byte("enc"), []byte("plain-enc"), + false, false, uuid.Nil, + nil, time.Now(), time.Now(), + ) + assert.False(t, msg.IsDeleted()) + msg.SoftDelete() + assert.True(t, msg.IsDeleted()) +} + +func TestMessage_Edit(t *testing.T) { + msg := chat.ReconstructMessage( + uuid.New(), uuid.New(), uuid.New(), + []byte("enc"), []byte("plain-enc"), + false, false, uuid.Nil, + nil, time.Now(), time.Now(), + ) + newEnc := []byte("new-encrypted-body") + newPlain := []byte("new-plain-enc") + old := msg.Edit(newEnc, newPlain) + assert.Equal(t, []byte("enc"), old) + assert.True(t, msg.IsEdited()) + assert.Equal(t, newEnc, msg.BodyEncrypted()) +} diff --git a/services/iam/internal/domain/chat/errors.go b/services/iam/internal/domain/chat/errors.go new file mode 100644 index 00000000..9837f6ff --- /dev/null +++ b/services/iam/internal/domain/chat/errors.go @@ -0,0 +1,36 @@ +package chat + +import "errors" + +// ErrConversationNotFound indicates the conversation does not exist. +var ErrConversationNotFound = errors.New("conversation not found") + +// ErrMessageNotFound indicates the message does not exist. +var ErrMessageNotFound = errors.New("message not found") + +// ErrNotParticipant indicates the user is not a participant. +var ErrNotParticipant = errors.New("user is not a participant of this conversation") + +// ErrNotAuthor indicates the user is not the message author. +var ErrNotAuthor = errors.New("user is not the author of this message") + +// ErrNotAdmin indicates the user lacks admin role. +var ErrNotAdmin = errors.New("user is not an admin of this conversation") + +// ErrDirectConversationFull indicates a direct conversation already has 2 participants. +var ErrDirectConversationFull = errors.New("direct conversation cannot have more than 2 participants") + +// ErrAlreadyParticipant indicates the user is already in the conversation. +var ErrAlreadyParticipant = errors.New("user is already a participant") + +// ErrMessageDeleted indicates the message has been soft-deleted. +var ErrMessageDeleted = errors.New("message has been deleted") + +// ErrParticipantLeft indicates the participant already left. +var ErrParticipantLeft = errors.New("participant has already left the conversation") + +// ErrAttachmentNotFound indicates the attachment does not exist. +var ErrAttachmentNotFound = errors.New("attachment not found") + +// ErrStorageUnavailable indicates the file storage service is not configured. +var ErrStorageUnavailable = errors.New("file storage is not available") diff --git a/services/iam/internal/domain/chat/repository.go b/services/iam/internal/domain/chat/repository.go new file mode 100644 index 00000000..188710ac --- /dev/null +++ b/services/iam/internal/domain/chat/repository.go @@ -0,0 +1,104 @@ +package chat + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +// ConversationRepository handles persistence for Conversation aggregates. +type ConversationRepository interface { + // Create inserts a new conversation with its initial participants. + Create(ctx context.Context, conv *Conversation) error + + // GetByID returns a conversation with all active participants. + GetByID(ctx context.Context, id uuid.UUID) (*Conversation, error) + + // FindDirect finds an existing DIRECT conversation between two users. + FindDirect(ctx context.Context, userA, userB uuid.UUID) (*Conversation, error) + + // ListByUserID returns conversations the user participates in, ordered by last message. + ListByUserID(ctx context.Context, userID uuid.UUID, page, pageSize int) ([]*Conversation, int64, error) + + // UpdateGroup persists name and avatar changes. + UpdateGroup(ctx context.Context, conv *Conversation) error + + // AddParticipants inserts new participant rows. + AddParticipants(ctx context.Context, conversationID uuid.UUID, participants []*Participant) error + + // RemoveParticipant sets left_at for a participant. + RemoveParticipant(ctx context.Context, conversationID, userID uuid.UUID) error + + // UpdateLastReadAt updates chat_participant.last_read_at for a user. + UpdateLastReadAt(ctx context.Context, conversationID, userID uuid.UUID, at time.Time) error + + // GetUnreadCounts returns, for each conversation ID, the count of messages + // created after userID's last_read_at (excluding userID's own messages). + GetUnreadCounts(ctx context.Context, conversationIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]int32, error) + + // ClearHistory sets history_cleared_at = NOW() for userID's participant row, + // hiding all prior messages from that user's view only. Other participants + // are unaffected. + ClearHistory(ctx context.Context, conversationID, userID uuid.UUID) error +} + +// MessageRepository handles persistence for Message aggregates. +type MessageRepository interface { + // Create inserts a new message. + Create(ctx context.Context, msg *Message) error + + // GetByID returns a message with its read receipts. + GetByID(ctx context.Context, id uuid.UUID) (*Message, error) + + // ListByConversation returns messages using cursor-based pagination (newest first). + // If afterTime is non-nil, only messages created strictly after it are returned — + // used to hide history a participant has cleared from their own view. + ListByConversation(ctx context.Context, conversationID uuid.UUID, pageSize int, beforeCursor string, afterTime *time.Time) ([]*Message, string, bool, error) + + // UpdateBody persists body changes after an edit. + UpdateBody(ctx context.Context, msg *Message) error + + // MarkDeleted sets is_deleted to true. + MarkDeleted(ctx context.Context, id uuid.UUID) error + + // SaveEditHistory inserts a pre-edit snapshot. + SaveEditHistory(ctx context.Context, entry *EditHistoryEntry) error + + // GetEditHistory returns all edit history for a message, newest first. + GetEditHistory(ctx context.Context, messageID uuid.UUID) ([]*EditHistoryEntry, error) + + // GetLastMessages returns, for each conversation ID, viewerID's most recent + // visible non-deleted message (one query, not N+1). Messages the viewer has + // cleared from their own history (created at or before their + // history_cleared_at) are excluded so the conversation preview matches the + // thread view. + GetLastMessages(ctx context.Context, conversationIDs []uuid.UUID, viewerID uuid.UUID) (map[uuid.UUID]*Message, error) +} + +// AttachmentRepository handles persistence for chat attachments. +type AttachmentRepository interface { + // Create inserts a new attachment. + Create(ctx context.Context, a *Attachment) error + + // GetByIDs returns attachments matching the given IDs (order not guaranteed). + GetByIDs(ctx context.Context, ids []uuid.UUID) ([]*Attachment, error) + + // LinkToMessage sets message_id for the given attachment IDs. + LinkToMessage(ctx context.Context, messageID uuid.UUID, attachmentIDs []uuid.UUID) error + + // ListByMessageIDs returns, for each message ID, its attachments (one query, not N+1). + ListByMessageIDs(ctx context.Context, messageIDs []uuid.UUID) (map[uuid.UUID][]*Attachment, error) +} + +// ReadReceiptRepository handles read receipts. +type ReadReceiptRepository interface { + // Upsert inserts or ignores a read receipt (idempotent). + Upsert(ctx context.Context, messageID, userID uuid.UUID) error + + // ListByMessage returns all receipts for a message. + ListByMessage(ctx context.Context, messageID uuid.UUID) ([]*ReadReceipt, error) + + // UpsertBulk marks multiple messages as read for a user. + UpsertBulk(ctx context.Context, messageIDs []uuid.UUID, userID uuid.UUID) error +} diff --git a/services/iam/internal/domain/chat/value_objects.go b/services/iam/internal/domain/chat/value_objects.go new file mode 100644 index 00000000..8ac5f281 --- /dev/null +++ b/services/iam/internal/domain/chat/value_objects.go @@ -0,0 +1,54 @@ +package chat + +// Type represents the conversation type. +type Type string + +const ( + // TypeDirect is a 1:1 direct message conversation. + TypeDirect Type = "DIRECT" + // TypeGroup is a multi-user group conversation. + TypeGroup Type = "GROUP" +) + +// String returns the string representation. +func (t Type) String() string { return string(t) } + +// ParseType parses a string into a Type. +func ParseType(s string) (Type, error) { + switch Type(s) { + case TypeDirect, TypeGroup: + return Type(s), nil + default: + return "", ErrConversationNotFound + } +} + +// Role represents a participant's role in a conversation. +type Role string + +const ( + // RoleOwner is the conversation creator. + RoleOwner Role = "OWNER" + // RoleAdmin can manage participants. + RoleAdmin Role = "ADMIN" + // RoleMember is a regular participant. + RoleMember Role = "MEMBER" +) + +// String returns the string representation. +func (r Role) String() string { return string(r) } + +// ParseRole parses a string into a Role. +func ParseRole(s string) (Role, error) { + switch Role(s) { + case RoleOwner, RoleAdmin, RoleMember: + return Role(s), nil + default: + return "", ErrNotParticipant + } +} + +// IsAdminOrOwner returns true if the role allows admin actions. +func (r Role) IsAdminOrOwner() bool { + return r == RoleOwner || r == RoleAdmin +} diff --git a/services/iam/internal/infrastructure/chat/broadcaster.go b/services/iam/internal/infrastructure/chat/broadcaster.go new file mode 100644 index 00000000..ee5c2508 --- /dev/null +++ b/services/iam/internal/infrastructure/chat/broadcaster.go @@ -0,0 +1,246 @@ +// Package chat provides in-memory + Redis Streams fan-out for chat events and +// Redis TTL-based presence tracking. +// +// Each authenticated user can have multiple subscribers (open SSE streams in +// different tabs). Publish fans out to all of a recipient's in-memory +// subscribers, then writes to a per-user Redis Stream so other IAM pods +// (and reconnecting clients) can replay missed events. +// +// Redis Streams (XADD/XREAD) replace the previous Pub/Sub approach: +// events are durable (trimmed to last 500 per user), replayable via +// lastEventId, and support multi-pod consumer reads without consumer groups +// (each pod reads independently using its own cursor). +package chat + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "os" + "sync" + "time" + + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "github.com/rs/zerolog/log" + "google.golang.org/protobuf/proto" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" +) + +const ( + chatStreamPrefix = "iam:chatstream:" + streamMaxLen = 500 + streamBlockTime = 5 * time.Second +) + +// Event is a chat event fanned out to subscribers. +type Event struct { + EventID string + UserID uuid.UUID + Response *iamv1.StreamChatEventsResponse +} + +// Broadcaster fans out chat events via in-memory channels and Redis Streams. +type Broadcaster struct { + mu sync.RWMutex + subs map[uuid.UUID]map[chan *Event]struct{} + rdb *redis.Client + selfPodID string +} + +// NewBroadcaster returns an in-memory-only Broadcaster. +func NewBroadcaster() *Broadcaster { + return &Broadcaster{ + subs: make(map[uuid.UUID]map[chan *Event]struct{}), + } +} + +// NewRedisBroadcaster returns a Broadcaster backed by Redis Streams. +func NewRedisBroadcaster(rdb *redis.Client) *Broadcaster { + return &Broadcaster{ + subs: make(map[uuid.UUID]map[chan *Event]struct{}), + rdb: rdb, + selfPodID: resolvePodID(), + } +} + +func resolvePodID() string { + h, err := os.Hostname() + if err != nil || h == "" { + return uuid.New().String() + } + return h +} + +// Subscribe registers a subscriber for a user and returns the event channel +// and an unsubscribe function. +func (b *Broadcaster) Subscribe(recipient uuid.UUID) (<-chan *Event, func()) { + const bufferSize = 32 + ch := make(chan *Event, bufferSize) + + b.mu.Lock() + if _, ok := b.subs[recipient]; !ok { + b.subs[recipient] = make(map[chan *Event]struct{}) + } + b.subs[recipient][ch] = struct{}{} + b.mu.Unlock() + + unsubMem := b.makeUnsubFunc(recipient, ch) + if b.rdb == nil { + return ch, unsubMem + } + + cancelRedis := b.bridgeRedisStream(recipient, ch) + return ch, func() { + cancelRedis() + unsubMem() + } +} + +func (b *Broadcaster) makeUnsubFunc(recipient uuid.UUID, ch chan *Event) func() { + return func() { + b.mu.Lock() + defer b.mu.Unlock() + set, ok := b.subs[recipient] + if !ok { + return + } + delete(set, ch) + if len(set) == 0 { + delete(b.subs, recipient) + } + close(ch) + } +} + +// Publish fans out an event in-memory and writes to Redis Stream. +func (b *Broadcaster) Publish(e *Event) { + if e == nil { + return + } + b.publishInMemory(e) + if b.rdb != nil { + b.publishToStream(e) + } +} + +// PublishToConversation fans out the same event to every participant. +func (b *Broadcaster) PublishToConversation(participantIDs []uuid.UUID, eventID string, resp *iamv1.StreamChatEventsResponse) { + for _, userID := range participantIDs { + b.Publish(&Event{ + EventID: fmt.Sprintf("%s-%s", eventID, userID), + UserID: userID, + Response: resp, + }) + } +} + +// SubscriberCount returns the number of in-memory subscribers for a user. +func (b *Broadcaster) SubscriberCount(recipient uuid.UUID) int { + b.mu.RLock() + defer b.mu.RUnlock() + return len(b.subs[recipient]) +} + +func (b *Broadcaster) publishInMemory(e *Event) { + b.mu.RLock() + defer b.mu.RUnlock() + set, ok := b.subs[e.UserID] + if !ok { + return + } + for ch := range set { + select { + case ch <- e: + default: + } + } +} + +func (b *Broadcaster) publishToStream(e *Event) { + protoBytes, err := proto.Marshal(e.Response) + if err != nil { + log.Warn().Err(err).Msg("broadcaster: marshal proto for stream") + return + } + streamKey := chatStreamPrefix + e.UserID.String() + err = b.rdb.XAdd(context.Background(), &redis.XAddArgs{ + Stream: streamKey, + MaxLen: streamMaxLen, + Approx: true, + Values: map[string]any{ + "eid": e.EventID, + "pod": b.selfPodID, + "data": base64.StdEncoding.EncodeToString(protoBytes), + }, + }).Err() + if err != nil { + log.Warn().Err(err).Msg("broadcaster: XADD to Redis Stream") + } +} + +// bridgeRedisStream reads from a per-user Redis Stream and forwards events +// from other pods into ch. Starts reading from "$" (new events only). +func (b *Broadcaster) bridgeRedisStream(recipient uuid.UUID, ch chan *Event) context.CancelFunc { + ctx, cancel := context.WithCancel(context.Background()) + streamKey := chatStreamPrefix + recipient.String() + + go func() { + lastID := "$" + for { + select { + case <-ctx.Done(): + return + default: + } + streams, err := b.rdb.XRead(ctx, &redis.XReadArgs{ + Streams: []string{streamKey, lastID}, + Block: streamBlockTime, + Count: 50, + }).Result() + if err != nil { + if errors.Is(err, redis.Nil) || ctx.Err() != nil { + continue + } + log.Warn().Err(err).Msg("broadcaster: XREAD error") + time.Sleep(time.Second) + continue + } + for _, stream := range streams { + for _, msg := range stream.Messages { + lastID = msg.ID + b.handleStreamMessage(msg, recipient, ch) + } + } + } + }() + + return cancel +} + +func (b *Broadcaster) handleStreamMessage(msg redis.XMessage, userID uuid.UUID, ch chan *Event) { + podID, _ := msg.Values["pod"].(string) //nolint:errcheck // absent/typed-wrong pod is treated as empty + if podID == b.selfPodID { + return + } + eventID, _ := msg.Values["eid"].(string) //nolint:errcheck // absent eid is treated as empty + dataB64, _ := msg.Values["data"].(string) //nolint:errcheck // absent data fails decode below + protoBytes, err := base64.StdEncoding.DecodeString(dataB64) + if err != nil { + log.Warn().Err(err).Msg("broadcaster: decode stream data") + return + } + resp := &iamv1.StreamChatEventsResponse{} + if err := proto.Unmarshal(protoBytes, resp); err != nil { + log.Warn().Err(err).Msg("broadcaster: unmarshal stream proto") + return + } + resp.EventId = eventID + evt := &Event{EventID: eventID, UserID: userID, Response: resp} + select { + case ch <- evt: + default: + } +} diff --git a/services/iam/internal/infrastructure/chat/broadcaster_test.go b/services/iam/internal/infrastructure/chat/broadcaster_test.go new file mode 100644 index 00000000..dffc6bbc --- /dev/null +++ b/services/iam/internal/infrastructure/chat/broadcaster_test.go @@ -0,0 +1,73 @@ +package chat_test + +import ( + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + iamv1 "github.com/mutugading/goapps-backend/gen/iam/v1" + chatinfra "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/chat" +) + +func TestBroadcaster_PublishReceive(t *testing.T) { + t.Parallel() + b := chatinfra.NewBroadcaster() + user := uuid.New() + ch, unsub := b.Subscribe(user) + defer unsub() + + resp := &iamv1.StreamChatEventsResponse{EventId: "test-event"} + ev := &chatinfra.Event{ + EventID: uuid.New().String(), + UserID: user, + Response: resp, + } + b.Publish(ev) + + select { + case got := <-ch: + assert.Equal(t, ev.EventID, got.EventID) + assert.Equal(t, ev.UserID, got.UserID) + assert.Equal(t, "test-event", got.Response.GetEventId()) + case <-time.After(100 * time.Millisecond): + t.Fatal("timed out waiting for chat event") + } +} + +func TestBroadcaster_MultipleSubscribers(t *testing.T) { + t.Parallel() + b := chatinfra.NewBroadcaster() + user := uuid.New() + + ch1, unsub1 := b.Subscribe(user) + defer unsub1() + ch2, unsub2 := b.Subscribe(user) + defer unsub2() + + assert.Equal(t, 2, b.SubscriberCount(user)) + + ev := &chatinfra.Event{ + EventID: uuid.New().String(), + UserID: user, + Response: &iamv1.StreamChatEventsResponse{EventId: "multi-test"}, + } + b.Publish(ev) + + var wg sync.WaitGroup + wg.Add(2) + for _, ch := range []<-chan *chatinfra.Event{ch1, ch2} { + go func(ch <-chan *chatinfra.Event) { + defer wg.Done() + select { + case got := <-ch: + assert.Equal(t, ev.EventID, got.EventID) + case <-time.After(100 * time.Millisecond): + t.Error("subscriber didn't receive event") + } + }(ch) + } + wg.Wait() +} diff --git a/services/iam/internal/infrastructure/chat/presence.go b/services/iam/internal/infrastructure/chat/presence.go new file mode 100644 index 00000000..36e48e23 --- /dev/null +++ b/services/iam/internal/infrastructure/chat/presence.go @@ -0,0 +1,140 @@ +package chat + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/redis/go-redis/v9" +) + +const ( + presenceKeyPrefix = "iam:presence:" + typingKeyPrefix = "iam:typing:" + presenceTTL = 90 * time.Second + typingTTL = 5 * time.Second +) + +// PresenceService tracks user online status and per-conversation typing +// indicators using short-TTL Redis keys. Presence/typing is best-effort: +// callers refresh via periodic heartbeats, and keys expire naturally when a +// client disconnects without an explicit "offline" signal. +type PresenceService struct { + rdb *redis.Client +} + +// NewPresenceService returns a PresenceService backed by rdb. +func NewPresenceService(rdb *redis.Client) *PresenceService { + return &PresenceService{rdb: rdb} +} + +func presenceKey(userID uuid.UUID) string { + return presenceKeyPrefix + userID.String() +} + +func typingKey(convID, userID uuid.UUID) string { + return typingKeyPrefix + convID.String() + ":" + userID.String() +} + +// SetOnline marks userID as online for presenceTTL. Callers should call this +// periodically (heartbeat) while the user has an active chat connection. +func (p *PresenceService) SetOnline(ctx context.Context, userID uuid.UUID) error { + if err := p.rdb.Set(ctx, presenceKey(userID), "1", presenceTTL).Err(); err != nil { + return fmt.Errorf("presence: set online for user %s: %w", userID, err) + } + return nil +} + +// IsOnline reports whether userID has an unexpired presence key. +func (p *PresenceService) IsOnline(ctx context.Context, userID uuid.UUID) (bool, error) { + n, err := p.rdb.Exists(ctx, presenceKey(userID)).Result() + if err != nil { + return false, fmt.Errorf("presence: check online for user %s: %w", userID, err) + } + return n > 0, nil +} + +// GetOnlineUsers returns the subset of userIDs that are currently online. If +// userIDs is empty, all currently online users are returned instead (found by +// scanning the presence key namespace). +func (p *PresenceService) GetOnlineUsers(ctx context.Context, userIDs []uuid.UUID) ([]uuid.UUID, error) { + if len(userIDs) == 0 { + return p.getAllOnlineUsers(ctx) + } + + keys := make([]string, len(userIDs)) + for i, id := range userIDs { + keys[i] = presenceKey(id) + } + + vals, err := p.rdb.MGet(ctx, keys...).Result() + if err != nil { + return nil, fmt.Errorf("presence: mget online users: %w", err) + } + + online := make([]uuid.UUID, 0, len(userIDs)) + for i, v := range vals { + if v != nil { + online = append(online, userIDs[i]) + } + } + return online, nil +} + +func (p *PresenceService) getAllOnlineUsers(ctx context.Context) ([]uuid.UUID, error) { + keys, err := p.rdb.Keys(ctx, presenceKeyPrefix+"*").Result() + if err != nil { + return nil, fmt.Errorf("presence: scan online users: %w", err) + } + + online := make([]uuid.UUID, 0, len(keys)) + for _, k := range keys { + idStr := strings.TrimPrefix(k, presenceKeyPrefix) + id, err := uuid.Parse(idStr) + if err != nil { + continue + } + online = append(online, id) + } + return online, nil +} + +// SetTyping marks userID as typing in convID for typingTTL, or immediately +// clears the typing indicator when isTyping is false. +func (p *PresenceService) SetTyping(ctx context.Context, convID, userID uuid.UUID, isTyping bool) error { + key := typingKey(convID, userID) + if !isTyping { + if err := p.rdb.Del(ctx, key).Err(); err != nil && !errors.Is(err, redis.Nil) { + return fmt.Errorf("presence: clear typing for user %s in conversation %s: %w", userID, convID, err) + } + return nil + } + if err := p.rdb.Set(ctx, key, "1", typingTTL).Err(); err != nil { + return fmt.Errorf("presence: set typing for user %s in conversation %s: %w", userID, convID, err) + } + return nil +} + +// GetTypingUsers returns the users currently typing in convID. +func (p *PresenceService) GetTypingUsers(ctx context.Context, convID uuid.UUID) ([]uuid.UUID, error) { + pattern := typingKeyPrefix + convID.String() + ":*" + keys, err := p.rdb.Keys(ctx, pattern).Result() + if err != nil { + return nil, fmt.Errorf("presence: scan typing users for conversation %s: %w", convID, err) + } + + prefix := typingKeyPrefix + convID.String() + ":" + typing := make([]uuid.UUID, 0, len(keys)) + for _, k := range keys { + idStr := strings.TrimPrefix(k, prefix) + id, err := uuid.Parse(idStr) + if err != nil { + continue + } + typing = append(typing, id) + } + return typing, nil +} diff --git a/services/iam/internal/infrastructure/config/config.go b/services/iam/internal/infrastructure/config/config.go index 8aab812c..7bcf1eb9 100644 --- a/services/iam/internal/infrastructure/config/config.go +++ b/services/iam/internal/infrastructure/config/config.go @@ -25,6 +25,12 @@ type Config struct { Tracing TracingConfig `mapstructure:"tracing"` RateLimit RateLimitConfig `mapstructure:"rate_limit"` Logger LoggerConfig `mapstructure:"logging"` + Chat ChatConfig `mapstructure:"chat"` +} + +// ChatConfig holds chat encryption configuration. +type ChatConfig struct { + MasterKey string `mapstructure:"master_key"` } // CORSConfig holds CORS configuration for SSO multi-app support. @@ -371,6 +377,8 @@ func bindEnvVars(v *viper.Viper) { {"storage.public_url", "STORAGE_PUBLIC_URL"}, // Internal service-to-service token (shared with trusted backends). {"security.internal_service_token", "INTERNAL_SERVICE_TOKEN"}, + // Chat encryption master key (32-byte hex string). + {"chat.master_key", "CHAT_MASTER_KEY"}, } for _, binding := range envBindings { diff --git a/services/iam/internal/infrastructure/crypto/message_encryptor.go b/services/iam/internal/infrastructure/crypto/message_encryptor.go new file mode 100644 index 00000000..08cabdcb --- /dev/null +++ b/services/iam/internal/infrastructure/crypto/message_encryptor.go @@ -0,0 +1,111 @@ +// Package crypto provides AES-256-GCM encryption for chat conversation keys +// and message bodies, wrapping per-conversation keys with a master key. +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "fmt" + "io" +) + +// Encryptor handles AES-256-GCM encryption for chat messages. +type Encryptor struct { + masterGCM cipher.AEAD +} + +// NewEncryptor creates an Encryptor from a 32-byte master key. +func NewEncryptor(masterKey []byte) (*Encryptor, error) { + if len(masterKey) != 32 { + return nil, fmt.Errorf("crypto: master key must be 32 bytes, got %d", len(masterKey)) + } + block, err := aes.NewCipher(masterKey) + if err != nil { + return nil, fmt.Errorf("crypto: create master cipher: %w", err) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("crypto: create master GCM: %w", err) + } + return &Encryptor{masterGCM: gcm}, nil +} + +// GenerateConversationKey generates a random 32-byte AES key for a new conversation. +func (e *Encryptor) GenerateConversationKey() ([]byte, error) { + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return nil, fmt.Errorf("crypto: generate conv key: %w", err) + } + return key, nil +} + +// EncryptConversationKey encrypts a conversation key with the master key. +func (e *Encryptor) EncryptConversationKey(convKey []byte) ([]byte, error) { + return e.seal(e.masterGCM, convKey) +} + +// DecryptConversationKey decrypts a stored conversation key using the master key. +func (e *Encryptor) DecryptConversationKey(encConvKey []byte) ([]byte, error) { + return e.open(e.masterGCM, encConvKey) +} + +// EncryptMessage encrypts a plaintext message body with the given conversation key. +func (e *Encryptor) EncryptMessage(convKey []byte, plaintext string) ([]byte, error) { + gcm, err := e.convGCM(convKey) + if err != nil { + return nil, err + } + return e.seal(gcm, []byte(plaintext)) +} + +// DecryptMessage decrypts a stored message ciphertext using the conversation key. +func (e *Encryptor) DecryptMessage(convKey, ciphertext []byte) (string, error) { + gcm, err := e.convGCM(convKey) + if err != nil { + return "", err + } + plain, err := e.open(gcm, ciphertext) + if err != nil { + return "", err + } + return string(plain), nil +} + +func (e *Encryptor) convGCM(convKey []byte) (cipher.AEAD, error) { + if len(convKey) != 32 { + return nil, fmt.Errorf("crypto: conv key must be 32 bytes, got %d", len(convKey)) + } + block, err := aes.NewCipher(convKey) + if err != nil { + return nil, fmt.Errorf("crypto: create conv cipher: %w", err) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("crypto: create conv GCM: %w", err) + } + return gcm, nil +} + +func (e *Encryptor) seal(gcm cipher.AEAD, plaintext []byte) ([]byte, error) { + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("crypto: generate nonce: %w", err) + } + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + return ciphertext, nil +} + +func (e *Encryptor) open(gcm cipher.AEAD, data []byte) ([]byte, error) { + ns := gcm.NonceSize() + if len(data) < ns { + return nil, errors.New("crypto: ciphertext too short") + } + nonce, ct := data[:ns], data[ns:] + plain, err := gcm.Open(nil, nonce, ct, nil) + if err != nil { + return nil, fmt.Errorf("crypto: decrypt failed (tampered?): %w", err) + } + return plain, nil +} diff --git a/services/iam/internal/infrastructure/crypto/message_encryptor_test.go b/services/iam/internal/infrastructure/crypto/message_encryptor_test.go new file mode 100644 index 00000000..9e5b64aa --- /dev/null +++ b/services/iam/internal/infrastructure/crypto/message_encryptor_test.go @@ -0,0 +1,58 @@ +package crypto_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/crypto" +) + +func TestEncryptor_RoundTrip(t *testing.T) { + masterKey := make([]byte, 32) + enc, err := crypto.NewEncryptor(masterKey) + require.NoError(t, err) + + convKey, err := enc.GenerateConversationKey() + require.NoError(t, err) + assert.Len(t, convKey, 32) + + encConvKey, err := enc.EncryptConversationKey(convKey) + require.NoError(t, err) + + decConvKey, err := enc.DecryptConversationKey(encConvKey) + require.NoError(t, err) + assert.Equal(t, convKey, decConvKey) + + ciphertext, err := enc.EncryptMessage(convKey, "hello world") + require.NoError(t, err) + assert.NotEmpty(t, ciphertext) + assert.NotContains(t, string(ciphertext), "hello world") + + plaintext, err := enc.DecryptMessage(convKey, ciphertext) + require.NoError(t, err) + assert.Equal(t, "hello world", plaintext) +} + +func TestEncryptor_TamperDetection(t *testing.T) { + masterKey := make([]byte, 32) + enc, err := crypto.NewEncryptor(masterKey) + require.NoError(t, err) + + convKey, err := enc.GenerateConversationKey() + require.NoError(t, err) + + ct, err := enc.EncryptMessage(convKey, "secret") + require.NoError(t, err) + + ct[len(ct)-1] ^= 0xFF + + _, err = enc.DecryptMessage(convKey, ct) + assert.Error(t, err, "tampered ciphertext must fail") +} + +func TestEncryptor_InvalidMasterKey(t *testing.T) { + _, err := crypto.NewEncryptor([]byte("too-short")) + assert.Error(t, err) +} diff --git a/services/iam/internal/infrastructure/postgres/chat_attachment_repository.go b/services/iam/internal/infrastructure/postgres/chat_attachment_repository.go new file mode 100644 index 00000000..c09f5fa1 --- /dev/null +++ b/services/iam/internal/infrastructure/postgres/chat_attachment_repository.go @@ -0,0 +1,166 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// ChatAttachmentRepository implements chat.AttachmentRepository. +type ChatAttachmentRepository struct { + db *DB +} + +// NewChatAttachmentRepository constructs the repo. +func NewChatAttachmentRepository(db *DB) *ChatAttachmentRepository { + return &ChatAttachmentRepository{db: db} +} + +// Create inserts a new attachment. +func (r *ChatAttachmentRepository) Create(ctx context.Context, a *chat.Attachment) error { + const q = ` + INSERT INTO chat_attachment + (attachment_id, conversation_id, message_id, uploader_user_id, + file_name, file_url, content_type, file_size, thumbnail_url, created_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)` + if _, err := r.db.ExecContext(ctx, q, + a.AttachmentID(), a.ConversationID(), a.MessageID(), a.UploaderUserID(), + a.FileName(), a.FileURL(), a.ContentType(), a.FileSize(), a.ThumbnailURL(), a.CreatedAt(), + ); err != nil { + return fmt.Errorf("chat attachment repo: create: %w", err) + } + return nil +} + +// GetByIDs returns attachments matching the given IDs. +func (r *ChatAttachmentRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]*chat.Attachment, error) { + if len(ids) == 0 { + return nil, nil + } + placeholders := make([]string, len(ids)) + args := make([]any, len(ids)) + for i, id := range ids { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args[i] = id + } + q := fmt.Sprintf(` + SELECT attachment_id, conversation_id, message_id, uploader_user_id, + file_name, file_url, content_type, file_size, thumbnail_url, created_at + FROM chat_attachment + WHERE attachment_id IN (%s)`, strings.Join(placeholders, ",")) + + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("chat attachment repo: get by ids: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat attachment repo: close get-by-ids rows") + } + }() + + var attachments []*chat.Attachment + for rows.Next() { + att, scanErr := scanAttachment(rows) + if scanErr != nil { + return nil, fmt.Errorf("chat attachment repo: scan: %w", scanErr) + } + attachments = append(attachments, att) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat attachment repo: get by ids rows err: %w", err) + } + return attachments, nil +} + +// LinkToMessage sets message_id for the given attachment IDs. +func (r *ChatAttachmentRepository) LinkToMessage(ctx context.Context, messageID uuid.UUID, attachmentIDs []uuid.UUID) error { + if len(attachmentIDs) == 0 { + return nil + } + placeholders := make([]string, len(attachmentIDs)) + args := make([]any, 0, len(attachmentIDs)+1) + args = append(args, messageID) + for i, id := range attachmentIDs { + placeholders[i] = fmt.Sprintf("$%d", i+2) + args = append(args, id) + } + q := fmt.Sprintf( + `UPDATE chat_attachment SET message_id=$1 WHERE attachment_id IN (%s)`, + strings.Join(placeholders, ","), + ) + if _, err := r.db.ExecContext(ctx, q, args...); err != nil { + return fmt.Errorf("chat attachment repo: link to message: %w", err) + } + return nil +} + +// ListByMessageIDs returns, for each message ID, its attachments. +func (r *ChatAttachmentRepository) ListByMessageIDs(ctx context.Context, messageIDs []uuid.UUID) (map[uuid.UUID][]*chat.Attachment, error) { + if len(messageIDs) == 0 { + return map[uuid.UUID][]*chat.Attachment{}, nil + } + placeholders := make([]string, len(messageIDs)) + args := make([]any, len(messageIDs)) + for i, id := range messageIDs { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args[i] = id + } + q := fmt.Sprintf(` + SELECT attachment_id, conversation_id, message_id, uploader_user_id, + file_name, file_url, content_type, file_size, thumbnail_url, created_at + FROM chat_attachment + WHERE message_id IN (%s) + ORDER BY created_at ASC`, strings.Join(placeholders, ",")) + + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("chat attachment repo: list by message ids: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat attachment repo: close list-by-message rows") + } + }() + + result := make(map[uuid.UUID][]*chat.Attachment, len(messageIDs)) + for rows.Next() { + att, scanErr := scanAttachment(rows) + if scanErr != nil { + return nil, fmt.Errorf("chat attachment repo: scan: %w", scanErr) + } + if att.MessageID() != nil { + result[*att.MessageID()] = append(result[*att.MessageID()], att) + } + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat attachment repo: list by message ids rows err: %w", err) + } + return result, nil +} + +func scanAttachment(s scanner) (*chat.Attachment, error) { + var ( + attachmentID, convID, uploaderID uuid.UUID + messageID *uuid.UUID + fileName, fileURL, contentType string + thumbnailURL sql.NullString + fileSize int64 + createdAt time.Time + ) + if err := s.Scan(&attachmentID, &convID, &messageID, &uploaderID, + &fileName, &fileURL, &contentType, &fileSize, &thumbnailURL, &createdAt); err != nil { + return nil, err + } + return chat.ReconstructAttachment( + attachmentID, convID, messageID, uploaderID, + fileName, fileURL, contentType, fileSize, thumbnailURL.String, createdAt, + ), nil +} diff --git a/services/iam/internal/infrastructure/postgres/chat_conversation_repository.go b/services/iam/internal/infrastructure/postgres/chat_conversation_repository.go new file mode 100644 index 00000000..fce55d17 --- /dev/null +++ b/services/iam/internal/infrastructure/postgres/chat_conversation_repository.go @@ -0,0 +1,341 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// ChatConversationRepository implements chat.ConversationRepository. +type ChatConversationRepository struct { + db *DB +} + +// NewChatConversationRepository constructs the repo. +func NewChatConversationRepository(db *DB) *ChatConversationRepository { + return &ChatConversationRepository{db: db} +} + +// Create inserts a new conversation with its initial participants. +func (r *ChatConversationRepository) Create(ctx context.Context, conv *chat.Conversation) error { + return r.db.Transaction(ctx, func(tx *sql.Tx) error { + const convQ = ` + INSERT INTO chat_conversation + (conversation_id, type, name, avatar_url, encryption_key, created_by, created_at, updated_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8)` + if _, err := tx.ExecContext(ctx, convQ, + conv.ID(), conv.Type().String(), conv.Name(), conv.AvatarURL(), + conv.EncryptionKey(), conv.CreatedBy(), conv.CreatedAt(), conv.UpdatedAt(), + ); err != nil { + return fmt.Errorf("insert conversation: %w", err) + } + + const partQ = ` + INSERT INTO chat_participant (conversation_id, user_id, role, joined_at) + VALUES ($1,$2,$3,$4)` + for _, p := range conv.Participants() { + if _, err := tx.ExecContext(ctx, partQ, + conv.ID(), p.UserID(), p.Role().String(), p.JoinedAt(), + ); err != nil { + return fmt.Errorf("insert participant: %w", err) + } + } + return nil + }) +} + +// GetByID returns a conversation with all active participants. +func (r *ChatConversationRepository) GetByID(ctx context.Context, id uuid.UUID) (*chat.Conversation, error) { + const q = ` + SELECT conversation_id, type, name, avatar_url, encryption_key, + created_by, created_at, updated_at, deleted_at + FROM chat_conversation + WHERE conversation_id = $1 AND deleted_at IS NULL` + + conv, err := r.scanConversation(r.db.QueryRowContext(ctx, q, id)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, chat.ErrConversationNotFound + } + return nil, fmt.Errorf("chat conv repo: get by id: %w", err) + } + + participants, err := r.loadParticipants(ctx, id) + if err != nil { + return nil, err + } + return chat.Reconstruct( + conv.ID(), conv.Type(), conv.Name(), conv.AvatarURL(), + conv.EncryptionKey(), conv.CreatedBy(), + conv.CreatedAt(), conv.UpdatedAt(), conv.DeletedAt(), + participants, + ), nil +} + +// FindDirect finds an existing DIRECT conversation between two users. +func (r *ChatConversationRepository) FindDirect(ctx context.Context, userA, userB uuid.UUID) (*chat.Conversation, error) { + const q = ` + SELECT cc.conversation_id + FROM chat_conversation cc + JOIN chat_participant pa ON pa.conversation_id = cc.conversation_id AND pa.user_id = $1 AND pa.left_at IS NULL + JOIN chat_participant pb ON pb.conversation_id = cc.conversation_id AND pb.user_id = $2 AND pb.left_at IS NULL + WHERE cc.type = 'DIRECT' AND cc.deleted_at IS NULL + LIMIT 1` + + var convID uuid.UUID + err := r.db.QueryRowContext(ctx, q, userA, userB).Scan(&convID) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil //nolint:nilnil // nil,nil means not found + } + if err != nil { + return nil, fmt.Errorf("chat conv repo: find direct: %w", err) + } + return r.GetByID(ctx, convID) +} + +// ListByUserID returns conversations the user participates in. +func (r *ChatConversationRepository) ListByUserID(ctx context.Context, userID uuid.UUID, page, pageSize int) ([]*chat.Conversation, int64, error) { + offset := (page - 1) * pageSize + + const listQ = ` + SELECT cc.conversation_id + FROM chat_conversation cc + JOIN chat_participant cp ON cp.conversation_id = cc.conversation_id + AND cp.user_id = $1 AND cp.left_at IS NULL + WHERE cc.deleted_at IS NULL + ORDER BY cc.updated_at DESC + LIMIT $2 OFFSET $3` + + rows, err := r.db.QueryContext(ctx, listQ, userID, pageSize, offset) + if err != nil { + return nil, 0, fmt.Errorf("chat conv repo: list: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat conv repo: close rows") + } + }() + + var ids []uuid.UUID + for rows.Next() { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return nil, 0, fmt.Errorf("chat conv repo: scan id: %w", err) + } + ids = append(ids, id) + } + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("chat conv repo: rows err: %w", err) + } + + const countQ = ` + SELECT COUNT(*) FROM chat_conversation cc + JOIN chat_participant cp ON cp.conversation_id = cc.conversation_id + AND cp.user_id = $1 AND cp.left_at IS NULL + WHERE cc.deleted_at IS NULL` + + var total int64 + if err := r.db.QueryRowContext(ctx, countQ, userID).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("chat conv repo: count: %w", err) + } + + convs := make([]*chat.Conversation, 0, len(ids)) + for _, id := range ids { + c, err := r.GetByID(ctx, id) + if err != nil { + return nil, 0, err + } + convs = append(convs, c) + } + return convs, total, nil +} + +// UpdateGroup persists name and avatar changes. +func (r *ChatConversationRepository) UpdateGroup(ctx context.Context, conv *chat.Conversation) error { + const q = ` + UPDATE chat_conversation SET name=$1, avatar_url=$2, updated_at=$3 + WHERE conversation_id=$4 AND deleted_at IS NULL` + if _, err := r.db.ExecContext(ctx, q, + conv.Name(), conv.AvatarURL(), time.Now().UTC(), conv.ID(), + ); err != nil { + return fmt.Errorf("chat conv repo: update group: %w", err) + } + return nil +} + +// AddParticipants inserts new participant rows. +func (r *ChatConversationRepository) AddParticipants(ctx context.Context, convID uuid.UUID, participants []*chat.Participant) error { + const q = ` + INSERT INTO chat_participant (conversation_id, user_id, role, joined_at) + VALUES ($1,$2,$3,$4) + ON CONFLICT (conversation_id, user_id) DO UPDATE SET left_at = NULL, role = $3` + for _, p := range participants { + if _, err := r.db.ExecContext(ctx, q, + convID, p.UserID(), p.Role().String(), p.JoinedAt(), + ); err != nil { + return fmt.Errorf("chat conv repo: add participant: %w", err) + } + } + return nil +} + +// RemoveParticipant sets left_at for a participant. +func (r *ChatConversationRepository) RemoveParticipant(ctx context.Context, convID, userID uuid.UUID) error { + const q = ` + UPDATE chat_participant SET left_at = NOW() + WHERE conversation_id=$1 AND user_id=$2 AND left_at IS NULL` + if _, err := r.db.ExecContext(ctx, q, convID, userID); err != nil { + return fmt.Errorf("chat conv repo: remove participant: %w", err) + } + return nil +} + +// UpdateLastReadAt updates chat_participant.last_read_at for a user. +func (r *ChatConversationRepository) UpdateLastReadAt(ctx context.Context, convID, userID uuid.UUID, at time.Time) error { + const q = `UPDATE chat_participant SET last_read_at=$1 WHERE conversation_id=$2 AND user_id=$3` + if _, err := r.db.ExecContext(ctx, q, at, convID, userID); err != nil { + return fmt.Errorf("chat conv repo: update last_read_at: %w", err) + } + return nil +} + +// GetUnreadCounts returns, for each conversation ID, the count of messages +// created after userID's last_read_at (excluding userID's own messages). +// Single batched query — avoids N+1 across the conversation list. +func (r *ChatConversationRepository) GetUnreadCounts(ctx context.Context, convIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]int32, error) { + if len(convIDs) == 0 { + return map[uuid.UUID]int32{}, nil + } + placeholders := make([]string, len(convIDs)) + args := make([]any, 0, len(convIDs)+1) + for i, id := range convIDs { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args = append(args, id) + } + userParam := len(convIDs) + 1 + args = append(args, userID) + + q := fmt.Sprintf(` + SELECT cm.conversation_id, COUNT(*) + FROM chat_message cm + JOIN chat_participant cp ON cp.conversation_id = cm.conversation_id AND cp.user_id = $%d + WHERE cm.conversation_id IN (%s) + AND cm.is_deleted = FALSE + AND cm.sender_user_id != $%d + AND (cp.last_read_at IS NULL OR cm.created_at > cp.last_read_at) + AND (cp.history_cleared_at IS NULL OR cm.created_at > cp.history_cleared_at) + GROUP BY cm.conversation_id`, userParam, strings.Join(placeholders, ","), userParam) + + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("chat conv repo: get unread counts: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat conv repo: close unread count rows") + } + }() + + counts := make(map[uuid.UUID]int32, len(convIDs)) + for rows.Next() { + var ( + convID uuid.UUID + count int64 + ) + if err := rows.Scan(&convID, &count); err != nil { + return nil, fmt.Errorf("chat conv repo: scan unread count: %w", err) + } + counts[convID] = safeInt64ToInt32Chat(count) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat conv repo: unread count rows err: %w", err) + } + return counts, nil +} + +// ClearHistory sets history_cleared_at = NOW() for userID's participant row. +func (r *ChatConversationRepository) ClearHistory(ctx context.Context, convID, userID uuid.UUID) error { + const q = `UPDATE chat_participant SET history_cleared_at = NOW() WHERE conversation_id=$1 AND user_id=$2` + if _, err := r.db.ExecContext(ctx, q, convID, userID); err != nil { + return fmt.Errorf("chat conv repo: clear history: %w", err) + } + return nil +} + +func (r *ChatConversationRepository) loadParticipants(ctx context.Context, convID uuid.UUID) ([]*chat.Participant, error) { + const q = ` + SELECT user_id, role, joined_at, left_at, last_read_at, history_cleared_at + FROM chat_participant WHERE conversation_id = $1` + + rows, err := r.db.QueryContext(ctx, q, convID) + if err != nil { + return nil, fmt.Errorf("chat conv repo: load participants: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat conv repo: close rows") + } + }() + + var parts []*chat.Participant + for rows.Next() { + var ( + userID uuid.UUID + roleStr string + joinedAt time.Time + leftAt, lastReadAt, clearedAt *time.Time + ) + if err := rows.Scan(&userID, &roleStr, &joinedAt, &leftAt, &lastReadAt, &clearedAt); err != nil { + return nil, fmt.Errorf("chat conv repo: scan participant: %w", err) + } + role, err := chat.ParseRole(roleStr) + if err != nil { + return nil, fmt.Errorf("chat conv repo: parse role: %w", err) + } + parts = append(parts, chat.ReconstructParticipant(convID, userID, role, joinedAt, leftAt, lastReadAt, clearedAt)) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat conv repo: participants rows err: %w", err) + } + return parts, nil +} + +func (r *ChatConversationRepository) scanConversation(row *sql.Row) (*chat.Conversation, error) { + var ( + id uuid.UUID + typeStr string + name sql.NullString + avatarURL sql.NullString + encryptionKey []byte + createdBy string + createdAt time.Time + updatedAt time.Time + deletedAt *time.Time + ) + if err := row.Scan(&id, &typeStr, &name, &avatarURL, &encryptionKey, + &createdBy, &createdAt, &updatedAt, &deletedAt); err != nil { + return nil, err + } + convType, err := chat.ParseType(typeStr) + if err != nil { + return nil, fmt.Errorf("scan conversation: parse type: %w", err) + } + return chat.Reconstruct(id, convType, name.String, avatarURL.String, encryptionKey, createdBy, createdAt, updatedAt, deletedAt, nil), nil +} + +// safeInt64ToInt32Chat clamps a COUNT(*) result into int32 range. +func safeInt64ToInt32Chat(v int64) int32 { + if v > math.MaxInt32 { + return math.MaxInt32 + } + return int32(v) //nolint:gosec // bounds checked above +} diff --git a/services/iam/internal/infrastructure/postgres/chat_message_repository.go b/services/iam/internal/infrastructure/postgres/chat_message_repository.go new file mode 100644 index 00000000..01253c23 --- /dev/null +++ b/services/iam/internal/infrastructure/postgres/chat_message_repository.go @@ -0,0 +1,497 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/base64" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/mutugading/goapps-backend/services/iam/internal/domain/chat" +) + +// ChatMessageRepository implements chat.MessageRepository. +type ChatMessageRepository struct { + db *DB +} + +// NewChatMessageRepository constructs the repo. +func NewChatMessageRepository(db *DB) *ChatMessageRepository { + return &ChatMessageRepository{db: db} +} + +// Create inserts a new message. +func (r *ChatMessageRepository) Create(ctx context.Context, msg *chat.Message) error { + var replyToID *uuid.UUID + if msg.ReplyToID() != uuid.Nil { + id := msg.ReplyToID() + replyToID = &id + } + const q = ` + INSERT INTO chat_message + (message_id, conversation_id, sender_user_id, + body_encrypted, body_plain_encrypted, + is_edited, is_deleted, reply_to_id, created_at, updated_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)` + if _, err := r.db.ExecContext(ctx, q, + msg.MessageID(), msg.ConversationID(), msg.SenderUserID(), + msg.BodyEncrypted(), msg.BodyPlainEncrypted(), + msg.IsEdited(), msg.IsDeleted(), replyToID, + msg.CreatedAt(), msg.UpdatedAt(), + ); err != nil { + return fmt.Errorf("chat msg repo: create: %w", err) + } + return nil +} + +// GetByID returns a message with its read receipts. +func (r *ChatMessageRepository) GetByID(ctx context.Context, id uuid.UUID) (*chat.Message, error) { + const q = ` + SELECT message_id, conversation_id, sender_user_id, + body_encrypted, body_plain_encrypted, + is_edited, is_deleted, reply_to_id, created_at, updated_at + FROM chat_message WHERE message_id = $1` + + msg, err := r.scanMessage(r.db.QueryRowContext(ctx, q, id)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, chat.ErrMessageNotFound + } + return nil, fmt.Errorf("chat msg repo: get by id: %w", err) + } + receipts, err := r.loadReceipts(ctx, id) + if err != nil { + return nil, err + } + return chat.ReconstructMessage( + msg.MessageID(), msg.ConversationID(), msg.SenderUserID(), + msg.BodyEncrypted(), msg.BodyPlainEncrypted(), + msg.IsEdited(), msg.IsDeleted(), msg.ReplyToID(), + receipts, msg.CreatedAt(), msg.UpdatedAt(), + ), nil +} + +// ListByConversation returns messages using cursor-based pagination (newest first). +// If afterTime is non-nil, only messages created strictly after it are included — +// used to hide history the caller has cleared from their own view. +func (r *ChatMessageRepository) ListByConversation(ctx context.Context, convID uuid.UUID, pageSize int, beforeCursor string, afterTime *time.Time) ([]*chat.Message, string, bool, error) { + fetchSize := pageSize + 1 + + rows, err := r.queryMessagePage(ctx, convID, fetchSize, beforeCursor, afterTime) + if err != nil { + return nil, "", false, err + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat msg repo: close rows") + } + }() + + var msgs []*chat.Message + for rows.Next() { + msg, scanErr := r.scanMessageRows(rows) + if scanErr != nil { + return nil, "", false, fmt.Errorf("chat msg repo: scan: %w", scanErr) + } + msgs = append(msgs, msg) + } + if err := rows.Err(); err != nil { + return nil, "", false, fmt.Errorf("chat msg repo: rows err: %w", err) + } + + hasMore := len(msgs) > pageSize + if hasMore { + msgs = msgs[:pageSize] + } + + if err := r.attachReceipts(ctx, msgs); err != nil { + return nil, "", false, err + } + + nextCursor := "" + if hasMore && len(msgs) > 0 { + last := msgs[len(msgs)-1] + nextCursor = encodeChatCursor(last.CreatedAt(), last.MessageID()) + } + return msgs, nextCursor, hasMore, nil +} + +const chatMessageSelectCols = ` + SELECT message_id, conversation_id, sender_user_id, + body_encrypted, body_plain_encrypted, + is_edited, is_deleted, reply_to_id, created_at, updated_at + FROM chat_message + WHERE conversation_id = $1 AND is_deleted = FALSE` + +// queryMessagePage dispatches to the first-page or cursor-paginated query, +// each optionally bounded by afterTime (the caller's cleared-history cutoff). +func (r *ChatMessageRepository) queryMessagePage(ctx context.Context, convID uuid.UUID, fetchSize int, beforeCursor string, afterTime *time.Time) (*sql.Rows, error) { + if beforeCursor == "" { + return r.queryFirstPage(ctx, convID, fetchSize, afterTime) + } + cursorTime, cursorID, parseErr := decodeChatCursor(beforeCursor) + if parseErr != nil { + return nil, fmt.Errorf("chat msg repo: invalid cursor: %w", parseErr) + } + return r.queryCursorPage(ctx, convID, fetchSize, cursorTime, cursorID, afterTime) +} + +func (r *ChatMessageRepository) queryFirstPage(ctx context.Context, convID uuid.UUID, fetchSize int, afterTime *time.Time) (*sql.Rows, error) { + if afterTime != nil { + return r.db.QueryContext(ctx, chatMessageSelectCols+` + AND created_at > $2 + ORDER BY created_at DESC, message_id DESC + LIMIT $3`, convID, *afterTime, fetchSize) + } + return r.db.QueryContext(ctx, chatMessageSelectCols+` + ORDER BY created_at DESC, message_id DESC + LIMIT $2`, convID, fetchSize) +} + +func (r *ChatMessageRepository) queryCursorPage(ctx context.Context, convID uuid.UUID, fetchSize int, cursorTime time.Time, cursorID uuid.UUID, afterTime *time.Time) (*sql.Rows, error) { + const cursorFilter = ` AND (created_at, message_id) < ($2, $3)` + if afterTime != nil { + return r.db.QueryContext(ctx, chatMessageSelectCols+cursorFilter+` + AND created_at > $4 + ORDER BY created_at DESC, message_id DESC + LIMIT $5`, convID, cursorTime, cursorID, *afterTime, fetchSize) + } + return r.db.QueryContext(ctx, chatMessageSelectCols+cursorFilter+` + ORDER BY created_at DESC, message_id DESC + LIMIT $4`, convID, cursorTime, cursorID, fetchSize) +} + +// attachReceipts batch-loads read receipts for all given messages (one query, +// not N+1) and reconstructs each message with its receipts attached. +func (r *ChatMessageRepository) attachReceipts(ctx context.Context, msgs []*chat.Message) error { + if len(msgs) == 0 { + return nil + } + placeholders := make([]string, len(msgs)) + args := make([]any, len(msgs)) + for i, msg := range msgs { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args[i] = msg.MessageID() + } + + q := fmt.Sprintf( + `SELECT message_id, user_id, read_at FROM chat_read_receipt WHERE message_id IN (%s)`, + strings.Join(placeholders, ","), + ) + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return fmt.Errorf("chat msg repo: batch load receipts: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat msg repo: close batch receipt rows") + } + }() + + receiptsByMsg := make(map[uuid.UUID][]*chat.ReadReceipt, len(msgs)) + for rows.Next() { + var ( + mID, uID uuid.UUID + readAt time.Time + ) + if err := rows.Scan(&mID, &uID, &readAt); err != nil { + return fmt.Errorf("chat msg repo: scan batch receipt: %w", err) + } + receiptsByMsg[mID] = append(receiptsByMsg[mID], chat.NewReadReceipt(mID, uID, readAt)) + } + if err := rows.Err(); err != nil { + return fmt.Errorf("chat msg repo: batch receipt rows err: %w", err) + } + + for i, msg := range msgs { + msgs[i] = chat.ReconstructMessage( + msg.MessageID(), msg.ConversationID(), msg.SenderUserID(), + msg.BodyEncrypted(), msg.BodyPlainEncrypted(), + msg.IsEdited(), msg.IsDeleted(), msg.ReplyToID(), + receiptsByMsg[msg.MessageID()], msg.CreatedAt(), msg.UpdatedAt(), + ) + } + return nil +} + +// UpdateBody persists body changes after an edit. +func (r *ChatMessageRepository) UpdateBody(ctx context.Context, msg *chat.Message) error { + const q = ` + UPDATE chat_message + SET body_encrypted=$1, body_plain_encrypted=$2, is_edited=TRUE, updated_at=NOW() + WHERE message_id=$3` + if _, err := r.db.ExecContext(ctx, q, + msg.BodyEncrypted(), msg.BodyPlainEncrypted(), msg.MessageID(), + ); err != nil { + return fmt.Errorf("chat msg repo: update body: %w", err) + } + return nil +} + +// MarkDeleted sets is_deleted to true. +func (r *ChatMessageRepository) MarkDeleted(ctx context.Context, id uuid.UUID) error { + const q = `UPDATE chat_message SET is_deleted=TRUE, updated_at=NOW() WHERE message_id=$1` + if _, err := r.db.ExecContext(ctx, q, id); err != nil { + return fmt.Errorf("chat msg repo: mark deleted: %w", err) + } + return nil +} + +// SaveEditHistory inserts a pre-edit snapshot. +func (r *ChatMessageRepository) SaveEditHistory(ctx context.Context, entry *chat.EditHistoryEntry) error { + const q = ` + INSERT INTO chat_message_edit_history (message_id, body_encrypted, edited_by, edited_at) + VALUES ($1,$2,$3,$4)` + if _, err := r.db.ExecContext(ctx, q, + entry.MessageID(), entry.BodyEncrypted(), entry.EditedBy(), entry.EditedAt(), + ); err != nil { + return fmt.Errorf("chat msg repo: save edit history: %w", err) + } + return nil +} + +// GetEditHistory returns all edit history for a message, newest first. +func (r *ChatMessageRepository) GetEditHistory(ctx context.Context, messageID uuid.UUID) ([]*chat.EditHistoryEntry, error) { + const q = ` + SELECT history_id, message_id, body_encrypted, edited_by, edited_at + FROM chat_message_edit_history + WHERE message_id=$1 ORDER BY edited_at DESC` + + rows, err := r.db.QueryContext(ctx, q, messageID) + if err != nil { + return nil, fmt.Errorf("chat msg repo: get edit history: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat msg repo: close rows") + } + }() + + var entries []*chat.EditHistoryEntry + for rows.Next() { + var ( + histID int64 + msgID, editedBy uuid.UUID + bodyEnc []byte + editedAt time.Time + ) + if err := rows.Scan(&histID, &msgID, &bodyEnc, &editedBy, &editedAt); err != nil { + return nil, fmt.Errorf("chat msg repo: scan edit history: %w", err) + } + entries = append(entries, chat.ReconstructEditHistory(histID, msgID, bodyEnc, editedBy, editedAt)) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat msg repo: edit history rows err: %w", err) + } + return entries, nil +} + +// GetLastMessages returns, for each conversation ID, its most recent +// non-deleted message. Single query via DISTINCT ON — avoids N+1. +func (r *ChatMessageRepository) GetLastMessages(ctx context.Context, convIDs []uuid.UUID, viewerID uuid.UUID) (map[uuid.UUID]*chat.Message, error) { + if len(convIDs) == 0 { + return map[uuid.UUID]*chat.Message{}, nil + } + placeholders := make([]string, len(convIDs)) + args := make([]any, 0, len(convIDs)+1) + for i, id := range convIDs { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args = append(args, id) + } + viewerParam := len(convIDs) + 1 + args = append(args, viewerID) + + // Join the viewer's participant row so a cleared history hides messages + // created at or before their history_cleared_at, matching the thread view. + q := fmt.Sprintf(` + SELECT DISTINCT ON (cm.conversation_id) + cm.message_id, cm.conversation_id, cm.sender_user_id, + cm.body_encrypted, cm.body_plain_encrypted, + cm.is_edited, cm.is_deleted, cm.reply_to_id, cm.created_at, cm.updated_at + FROM chat_message cm + JOIN chat_participant cp + ON cp.conversation_id = cm.conversation_id AND cp.user_id = $%d + WHERE cm.conversation_id IN (%s) AND cm.is_deleted = FALSE + AND (cp.history_cleared_at IS NULL OR cm.created_at > cp.history_cleared_at) + ORDER BY cm.conversation_id, cm.created_at DESC, cm.message_id DESC`, + viewerParam, strings.Join(placeholders, ",")) + + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("chat msg repo: get last messages: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat msg repo: close last message rows") + } + }() + + result := make(map[uuid.UUID]*chat.Message, len(convIDs)) + for rows.Next() { + msg, scanErr := r.scanMessageRows(rows) + if scanErr != nil { + return nil, fmt.Errorf("chat msg repo: scan last message: %w", scanErr) + } + result[msg.ConversationID()] = msg + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat msg repo: last message rows err: %w", err) + } + return result, nil +} + +func (r *ChatMessageRepository) loadReceipts(ctx context.Context, msgID uuid.UUID) ([]*chat.ReadReceipt, error) { + const q = `SELECT message_id, user_id, read_at FROM chat_read_receipt WHERE message_id=$1` + rows, err := r.db.QueryContext(ctx, q, msgID) + if err != nil { + return nil, fmt.Errorf("chat msg repo: load receipts: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat msg repo: close rows") + } + }() + var receipts []*chat.ReadReceipt + for rows.Next() { + var ( + mID, uID uuid.UUID + readAt time.Time + ) + if err := rows.Scan(&mID, &uID, &readAt); err != nil { + return nil, fmt.Errorf("chat msg repo: scan receipt: %w", err) + } + receipts = append(receipts, chat.NewReadReceipt(mID, uID, readAt)) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat msg repo: receipt rows err: %w", err) + } + return receipts, nil +} + +func (r *ChatMessageRepository) scanMessage(row *sql.Row) (*chat.Message, error) { + var ( + msgID, convID, senderID uuid.UUID + bodyEnc, bodyPlainEnc []byte + isEdited, isDeleted bool + replyToID *uuid.UUID + createdAt, updatedAt time.Time + ) + if err := row.Scan(&msgID, &convID, &senderID, &bodyEnc, &bodyPlainEnc, + &isEdited, &isDeleted, &replyToID, &createdAt, &updatedAt); err != nil { + return nil, err + } + replyTo := uuid.Nil + if replyToID != nil { + replyTo = *replyToID + } + return chat.ReconstructMessage(msgID, convID, senderID, bodyEnc, bodyPlainEnc, + isEdited, isDeleted, replyTo, nil, createdAt, updatedAt), nil +} + +func (r *ChatMessageRepository) scanMessageRows(rows *sql.Rows) (*chat.Message, error) { + var ( + msgID, convID, senderID uuid.UUID + bodyEnc, bodyPlainEnc []byte + isEdited, isDeleted bool + replyToID *uuid.UUID + createdAt, updatedAt time.Time + ) + if err := rows.Scan(&msgID, &convID, &senderID, &bodyEnc, &bodyPlainEnc, + &isEdited, &isDeleted, &replyToID, &createdAt, &updatedAt); err != nil { + return nil, err + } + replyTo := uuid.Nil + if replyToID != nil { + replyTo = *replyToID + } + return chat.ReconstructMessage(msgID, convID, senderID, bodyEnc, bodyPlainEnc, + isEdited, isDeleted, replyTo, nil, createdAt, updatedAt), nil +} + +// ChatReadReceiptRepository implements chat.ReadReceiptRepository. +type ChatReadReceiptRepository struct { + db *DB +} + +// NewChatReadReceiptRepository constructs the repo. +func NewChatReadReceiptRepository(db *DB) *ChatReadReceiptRepository { + return &ChatReadReceiptRepository{db: db} +} + +// Upsert inserts or ignores a read receipt (idempotent). +func (r *ChatReadReceiptRepository) Upsert(ctx context.Context, messageID, userID uuid.UUID) error { + const q = `INSERT INTO chat_read_receipt (message_id, user_id, read_at) VALUES ($1,$2,NOW()) ON CONFLICT DO NOTHING` + if _, err := r.db.ExecContext(ctx, q, messageID, userID); err != nil { + return fmt.Errorf("chat receipt repo: upsert: %w", err) + } + return nil +} + +// ListByMessage returns all receipts for a message. +func (r *ChatReadReceiptRepository) ListByMessage(ctx context.Context, msgID uuid.UUID) ([]*chat.ReadReceipt, error) { + const q = `SELECT message_id, user_id, read_at FROM chat_read_receipt WHERE message_id=$1` + rows, err := r.db.QueryContext(ctx, q, msgID) + if err != nil { + return nil, fmt.Errorf("chat receipt repo: list: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat msg repo: close rows") + } + }() + var receipts []*chat.ReadReceipt + for rows.Next() { + var ( + mID, uID uuid.UUID + readAt time.Time + ) + if err := rows.Scan(&mID, &uID, &readAt); err != nil { + return nil, fmt.Errorf("chat receipt repo: scan: %w", err) + } + receipts = append(receipts, chat.NewReadReceipt(mID, uID, readAt)) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat receipt repo: rows err: %w", err) + } + return receipts, nil +} + +// UpsertBulk marks multiple messages as read for a user. +func (r *ChatReadReceiptRepository) UpsertBulk(ctx context.Context, messageIDs []uuid.UUID, userID uuid.UUID) error { + for _, msgID := range messageIDs { + if err := r.Upsert(ctx, msgID, userID); err != nil { + return err + } + } + return nil +} + +func encodeChatCursor(createdAt time.Time, messageID uuid.UUID) string { + raw := createdAt.UTC().Format(time.RFC3339Nano) + "|" + messageID.String() + return base64.StdEncoding.EncodeToString([]byte(raw)) +} + +func decodeChatCursor(cursor string) (time.Time, uuid.UUID, error) { + b, err := base64.StdEncoding.DecodeString(cursor) + if err != nil { + return time.Time{}, uuid.Nil, fmt.Errorf("decode base64: %w", err) + } + parts := strings.SplitN(string(b), "|", 2) + if len(parts) != 2 { + return time.Time{}, uuid.Nil, fmt.Errorf("invalid cursor format") + } + t, err := time.Parse(time.RFC3339Nano, parts[0]) + if err != nil { + return time.Time{}, uuid.Nil, fmt.Errorf("parse time: %w", err) + } + id, err := uuid.Parse(parts[1]) + if err != nil { + return time.Time{}, uuid.Nil, fmt.Errorf("parse uuid: %w", err) + } + return t, id, nil +} diff --git a/services/iam/internal/infrastructure/postgres/chat_user_resolver.go b/services/iam/internal/infrastructure/postgres/chat_user_resolver.go new file mode 100644 index 00000000..9066180f --- /dev/null +++ b/services/iam/internal/infrastructure/postgres/chat_user_resolver.go @@ -0,0 +1,75 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/google/uuid" + "github.com/rs/zerolog/log" +) + +// ChatUserInfo holds lightweight user data for chat participant display. +type ChatUserInfo struct { + UserID uuid.UUID + Username string + FullName string + AvatarURL string +} + +// ChatUserResolver resolves user IDs to display info for chat participants. +type ChatUserResolver struct { + db *DB +} + +// NewChatUserResolver constructs the resolver. +func NewChatUserResolver(db *DB) *ChatUserResolver { + return &ChatUserResolver{db: db} +} + +// ResolveUsers batch-resolves user IDs to display info. +func (r *ChatUserResolver) ResolveUsers(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID]*ChatUserInfo, error) { + if len(userIDs) == 0 { + return map[uuid.UUID]*ChatUserInfo{}, nil + } + placeholders := make([]string, len(userIDs)) + args := make([]any, len(userIDs)) + for i, id := range userIDs { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args[i] = id + } + q := fmt.Sprintf(` + SELECT u.user_id, u.username, + COALESCE(d.full_name, ''), + COALESCE(d.profile_picture_url, '') + FROM mst_user u + LEFT JOIN mst_user_detail d ON d.user_id = u.user_id + WHERE u.user_id IN (%s)`, strings.Join(placeholders, ",")) + + rows, err := r.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, fmt.Errorf("chat user resolver: query: %w", err) + } + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("chat user resolver: close rows") + } + }() + + result := make(map[uuid.UUID]*ChatUserInfo, len(userIDs)) + for rows.Next() { + var info ChatUserInfo + var fullName, avatarURL sql.NullString + if err := rows.Scan(&info.UserID, &info.Username, &fullName, &avatarURL); err != nil { + return nil, fmt.Errorf("chat user resolver: scan: %w", err) + } + info.FullName = fullName.String + info.AvatarURL = avatarURL.String + result[info.UserID] = &info + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("chat user resolver: rows err: %w", err) + } + return result, nil +} diff --git a/services/iam/internal/infrastructure/postgres/chatbot_audit_repository.go b/services/iam/internal/infrastructure/postgres/chatbot_audit_repository.go new file mode 100644 index 00000000..ee476226 --- /dev/null +++ b/services/iam/internal/infrastructure/postgres/chatbot_audit_repository.go @@ -0,0 +1,46 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/google/uuid" +) + +// ChatbotAuditEntry represents one chatbot interaction. +type ChatbotAuditEntry struct { + UserID uuid.UUID + SessionID string + RequestTokens int + ResponseTokens int + ToolsCalled []string + WasBlocked bool + BlockReason string +} + +// ChatbotAuditRepository persists chatbot audit logs. +type ChatbotAuditRepository struct { + db *DB +} + +// NewChatbotAuditRepository constructs the repo. +func NewChatbotAuditRepository(db *DB) *ChatbotAuditRepository { + return &ChatbotAuditRepository{db: db} +} + +// Create inserts a new audit log entry. +func (r *ChatbotAuditRepository) Create(ctx context.Context, entry *ChatbotAuditEntry) error { + const q = ` + INSERT INTO chatbot_audit_log + (user_id, session_id, request_tokens, response_tokens, tools_called, was_blocked, block_reason) + VALUES ($1,$2,$3,$4,$5,$6,$7)` + toolsStr := "{" + strings.Join(entry.ToolsCalled, ",") + "}" + if _, err := r.db.ExecContext(ctx, q, + entry.UserID, entry.SessionID, entry.RequestTokens, entry.ResponseTokens, + toolsStr, entry.WasBlocked, entry.BlockReason, + ); err != nil { + return fmt.Errorf("chatbot audit repo: create: %w", err) + } + return nil +} diff --git a/services/iam/internal/infrastructure/storage/storage.go b/services/iam/internal/infrastructure/storage/storage.go index 7c9410bf..f4bf05f9 100644 --- a/services/iam/internal/infrastructure/storage/storage.go +++ b/services/iam/internal/infrastructure/storage/storage.go @@ -22,6 +22,8 @@ type Service interface { UploadProfilePicture(ctx context.Context, userID string, filename string, data io.Reader, size int64, contentType string) (string, error) // UploadCMSImage uploads a CMS image and returns the object URL. UploadCMSImage(ctx context.Context, folder string, filename string, data io.Reader, size int64, contentType string) (string, error) + // UploadChatAttachment uploads a chat attachment and returns the object URL. + UploadChatAttachment(ctx context.Context, convID string, filename string, data io.Reader, size int64, contentType string) (string, error) // DeleteObject deletes a file by its object key. DeleteObject(ctx context.Context, objectKey string) error // ExtractObjectKey extracts the object key from a full URL. @@ -194,6 +196,36 @@ func (s *MinIOService) UploadCMSImage(ctx context.Context, folder string, filena return objectURL, nil } +// UploadChatAttachment uploads a chat attachment to MinIO. +// Files are stored as: {basePath}/chat/attachments/{convID}/{uuid}{ext} +// Example with basePath="iam": iam/chat/attachments/conv-id/uuid.pdf +func (s *MinIOService) UploadChatAttachment(ctx context.Context, convID string, filename string, data io.Reader, size int64, contentType string) (string, error) { + ext := path.Ext(filename) + objectName := fmt.Sprintf("%s/chat/attachments/%s/%s%s", s.basePath, convID, uuid.New().String(), ext) + + _, err := s.client.PutObject(ctx, s.bucket, objectName, data, size, minio.PutObjectOptions{ + ContentType: contentType, + }) + if err != nil { + return "", fmt.Errorf("failed to upload chat attachment: %w", err) + } + + var objectURL string + if s.publicURL != "" { + objectURL = fmt.Sprintf("%s/%s/%s", strings.TrimRight(s.publicURL, "/"), s.bucket, objectName) + } else { + objectURL = fmt.Sprintf("%s/%s/%s", s.client.EndpointURL().String(), s.bucket, objectName) + } + + log.Debug(). + Str("conv_id", convID). + Str("object", objectName). + Str("url", objectURL). + Msg("chat attachment uploaded") + + return objectURL, nil +} + // DeleteObject deletes a file from MinIO by its object key. func (s *MinIOService) DeleteObject(ctx context.Context, objectKey string) error { if objectKey == "" { diff --git a/services/iam/migrations/postgres/000070_create_chat_conversation.down.sql b/services/iam/migrations/postgres/000070_create_chat_conversation.down.sql new file mode 100644 index 00000000..fe84048b --- /dev/null +++ b/services/iam/migrations/postgres/000070_create_chat_conversation.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chat_conversation; diff --git a/services/iam/migrations/postgres/000070_create_chat_conversation.up.sql b/services/iam/migrations/postgres/000070_create_chat_conversation.up.sql new file mode 100644 index 00000000..eceb5302 --- /dev/null +++ b/services/iam/migrations/postgres/000070_create_chat_conversation.up.sql @@ -0,0 +1,17 @@ +-- 000070: Create chat_conversation table for direct and group conversations. +CREATE TABLE IF NOT EXISTS chat_conversation ( + conversation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(10) NOT NULL CHECK (type IN ('DIRECT', 'GROUP')), + name VARCHAR(200), + avatar_url VARCHAR(500), + encryption_key BYTEA NOT NULL, + created_by VARCHAR(100) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + deleted_by VARCHAR(100) +); + +CREATE INDEX IF NOT EXISTS idx_chat_conversation_created + ON chat_conversation(created_at DESC) + WHERE deleted_at IS NULL; diff --git a/services/iam/migrations/postgres/000071_create_chat_participant.down.sql b/services/iam/migrations/postgres/000071_create_chat_participant.down.sql new file mode 100644 index 00000000..1fa55304 --- /dev/null +++ b/services/iam/migrations/postgres/000071_create_chat_participant.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chat_participant; diff --git a/services/iam/migrations/postgres/000071_create_chat_participant.up.sql b/services/iam/migrations/postgres/000071_create_chat_participant.up.sql new file mode 100644 index 00000000..6252c874 --- /dev/null +++ b/services/iam/migrations/postgres/000071_create_chat_participant.up.sql @@ -0,0 +1,15 @@ +-- 000071: Create chat_participant table linking users to conversations. +CREATE TABLE IF NOT EXISTS chat_participant ( + conversation_id UUID NOT NULL REFERENCES chat_conversation(conversation_id), + user_id UUID NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'MEMBER' + CHECK (role IN ('OWNER', 'ADMIN', 'MEMBER')), + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + left_at TIMESTAMPTZ, + last_read_at TIMESTAMPTZ, + PRIMARY KEY (conversation_id, user_id) +); + +CREATE INDEX IF NOT EXISTS idx_chat_participant_user + ON chat_participant(user_id) + WHERE left_at IS NULL; diff --git a/services/iam/migrations/postgres/000072_create_chat_message.down.sql b/services/iam/migrations/postgres/000072_create_chat_message.down.sql new file mode 100644 index 00000000..6643ebb8 --- /dev/null +++ b/services/iam/migrations/postgres/000072_create_chat_message.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chat_message; diff --git a/services/iam/migrations/postgres/000072_create_chat_message.up.sql b/services/iam/migrations/postgres/000072_create_chat_message.up.sql new file mode 100644 index 00000000..3f17ed94 --- /dev/null +++ b/services/iam/migrations/postgres/000072_create_chat_message.up.sql @@ -0,0 +1,20 @@ +-- 000072: Create chat_message table with encrypted body storage. +CREATE TABLE IF NOT EXISTS chat_message ( + message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES chat_conversation(conversation_id), + sender_user_id UUID NOT NULL, + body_encrypted BYTEA NOT NULL, + body_plain_encrypted BYTEA NOT NULL, + is_edited BOOLEAN NOT NULL DEFAULT FALSE, + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + reply_to_id UUID REFERENCES chat_message(message_id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_chat_message_conv_created + ON chat_message(conversation_id, created_at DESC) + WHERE is_deleted = FALSE; + +CREATE INDEX IF NOT EXISTS idx_chat_message_sender + ON chat_message(sender_user_id); diff --git a/services/iam/migrations/postgres/000073_create_chat_message_edit_history.down.sql b/services/iam/migrations/postgres/000073_create_chat_message_edit_history.down.sql new file mode 100644 index 00000000..9a22c106 --- /dev/null +++ b/services/iam/migrations/postgres/000073_create_chat_message_edit_history.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chat_message_edit_history; diff --git a/services/iam/migrations/postgres/000073_create_chat_message_edit_history.up.sql b/services/iam/migrations/postgres/000073_create_chat_message_edit_history.up.sql new file mode 100644 index 00000000..74e2199d --- /dev/null +++ b/services/iam/migrations/postgres/000073_create_chat_message_edit_history.up.sql @@ -0,0 +1,11 @@ +-- 000073: Create chat_message_edit_history table for message edit tracking. +CREATE TABLE IF NOT EXISTS chat_message_edit_history ( + history_id BIGSERIAL PRIMARY KEY, + message_id UUID NOT NULL REFERENCES chat_message(message_id), + body_encrypted BYTEA NOT NULL, + edited_by UUID NOT NULL, + edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_chat_edit_history_msg + ON chat_message_edit_history(message_id, edited_at DESC); diff --git a/services/iam/migrations/postgres/000074_create_chat_read_receipt.down.sql b/services/iam/migrations/postgres/000074_create_chat_read_receipt.down.sql new file mode 100644 index 00000000..c9698a56 --- /dev/null +++ b/services/iam/migrations/postgres/000074_create_chat_read_receipt.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chat_read_receipt; diff --git a/services/iam/migrations/postgres/000074_create_chat_read_receipt.up.sql b/services/iam/migrations/postgres/000074_create_chat_read_receipt.up.sql new file mode 100644 index 00000000..2a9c262d --- /dev/null +++ b/services/iam/migrations/postgres/000074_create_chat_read_receipt.up.sql @@ -0,0 +1,10 @@ +-- 000074: Create chat_read_receipt table for per-message read tracking. +CREATE TABLE IF NOT EXISTS chat_read_receipt ( + message_id UUID NOT NULL REFERENCES chat_message(message_id), + user_id UUID NOT NULL, + read_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (message_id, user_id) +); + +CREATE INDEX IF NOT EXISTS idx_chat_read_receipt_msg + ON chat_read_receipt(message_id); diff --git a/services/iam/migrations/postgres/000075_create_chatbot_audit_log.down.sql b/services/iam/migrations/postgres/000075_create_chatbot_audit_log.down.sql new file mode 100644 index 00000000..ca620aa6 --- /dev/null +++ b/services/iam/migrations/postgres/000075_create_chatbot_audit_log.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS chatbot_audit_log; diff --git a/services/iam/migrations/postgres/000075_create_chatbot_audit_log.up.sql b/services/iam/migrations/postgres/000075_create_chatbot_audit_log.up.sql new file mode 100644 index 00000000..2c4449d0 --- /dev/null +++ b/services/iam/migrations/postgres/000075_create_chatbot_audit_log.up.sql @@ -0,0 +1,15 @@ +-- 000075: Create chatbot_audit_log table for AI chatbot usage tracking. +CREATE TABLE IF NOT EXISTS chatbot_audit_log ( + log_id BIGSERIAL PRIMARY KEY, + user_id UUID NOT NULL, + session_id VARCHAR(100) NOT NULL, + request_tokens INT NOT NULL DEFAULT 0, + response_tokens INT NOT NULL DEFAULT 0, + tools_called TEXT[], + was_blocked BOOLEAN NOT NULL DEFAULT FALSE, + block_reason VARCHAR(200), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_chatbot_audit_user_date + ON chatbot_audit_log(user_id, created_at DESC); diff --git a/services/iam/migrations/postgres/000076_chat_menu_permissions.down.sql b/services/iam/migrations/postgres/000076_chat_menu_permissions.down.sql new file mode 100644 index 00000000..7036b9e7 --- /dev/null +++ b/services/iam/migrations/postgres/000076_chat_menu_permissions.down.sql @@ -0,0 +1,32 @@ +-- 000076 down: Remove chat menu entries and permissions. + +DELETE FROM role_permissions +WHERE permission_id IN ( + SELECT permission_id FROM mst_permission + WHERE permission_code IN ( + 'iam.chat.message.view', + 'iam.chat.message.create', + 'iam.chat.message.delete', + 'iam.chatbot.assistant.view' + ) +); + +DELETE FROM menu_permissions +WHERE menu_id IN ( + '00000000-0000-0000-0001-000000000008', + '00000000-0000-0000-0002-000000000017' +); + +DELETE FROM mst_permission +WHERE permission_code IN ( + 'iam.chat.message.view', + 'iam.chat.message.create', + 'iam.chat.message.delete', + 'iam.chatbot.assistant.view' +); + +DELETE FROM mst_menu +WHERE menu_id IN ( + '00000000-0000-0000-0001-000000000008', + '00000000-0000-0000-0002-000000000017' +); diff --git a/services/iam/migrations/postgres/000076_chat_menu_permissions.up.sql b/services/iam/migrations/postgres/000076_chat_menu_permissions.up.sql new file mode 100644 index 00000000..8eff0d24 --- /dev/null +++ b/services/iam/migrations/postgres/000076_chat_menu_permissions.up.sql @@ -0,0 +1,73 @@ +-- 000076: Seed chat menu entries and permissions. + +-- Level-1: Chat root menu (top-level sidebar section, like Dashboard/Finance) +INSERT INTO mst_menu (menu_id, parent_id, menu_code, menu_title, menu_url, icon_name, service_name, menu_level, sort_order, is_visible, is_active, created_by) +VALUES ( + '00000000-0000-0000-0001-000000000008', + NULL, + 'CHAT', + 'Chat', + NULL, + 'MessageSquare', + 'iam', + 1, + 80, + TRUE, + TRUE, + 'system' +) ON CONFLICT (menu_code) DO NOTHING; + +-- Level-2: /chat page (child of Chat root) +INSERT INTO mst_menu (menu_id, parent_id, menu_code, menu_title, menu_url, icon_name, service_name, menu_level, sort_order, is_visible, is_active, created_by) +VALUES ( + '00000000-0000-0000-0002-000000000017', + '00000000-0000-0000-0001-000000000008', + 'CHAT_PAGE', + 'All Conversations', + '/chat', + 'MessageSquare', + 'iam', + 2, + 1, + TRUE, + TRUE, + 'system' +) ON CONFLICT (menu_code) DO NOTHING; + +-- Permissions +INSERT INTO mst_permission (permission_id, permission_code, permission_name, description, service_name, module_name, action_type, created_by) +VALUES + (gen_random_uuid(), 'iam.chat.message.view', 'Chat View', 'View chat conversations and messages.', 'iam', 'chat', 'view', 'system'), + (gen_random_uuid(), 'iam.chat.message.create', 'Chat Create Message', 'Send messages in chat.', 'iam', 'chat', 'create', 'system'), + (gen_random_uuid(), 'iam.chat.message.delete', 'Chat Delete Message', 'Delete own messages in chat.', 'iam', 'chat', 'delete', 'system'), + (gen_random_uuid(), 'iam.chatbot.assistant.view', 'Chatbot Access', 'Access the AI chatbot assistant.', 'iam', 'chatbot', 'view', 'system') +ON CONFLICT (permission_code) DO NOTHING; + +-- Wire menu → permission (CHAT_PAGE visible to anyone with chat.message.view) +INSERT INTO menu_permissions (id, menu_id, permission_id, assigned_by) +SELECT + gen_random_uuid(), + '00000000-0000-0000-0002-000000000017', + permission_id, + 'system' +FROM mst_permission +WHERE permission_code = 'iam.chat.message.view' +ON CONFLICT (menu_id, permission_id) DO NOTHING; + +-- Assign all chat permissions to SUPER_ADMIN role +INSERT INTO role_permissions (id, role_id, permission_id, assigned_by) +SELECT + gen_random_uuid(), + r.role_id, + p.permission_id, + 'system' +FROM mst_role r +CROSS JOIN mst_permission p +WHERE r.role_code = 'SUPER_ADMIN' + AND p.permission_code IN ( + 'iam.chat.message.view', + 'iam.chat.message.create', + 'iam.chat.message.delete', + 'iam.chatbot.assistant.view' + ) +ON CONFLICT (role_id, permission_id) DO NOTHING; diff --git a/services/iam/migrations/postgres/000077_add_history_cleared_at.down.sql b/services/iam/migrations/postgres/000077_add_history_cleared_at.down.sql new file mode 100644 index 00000000..a97f03ad --- /dev/null +++ b/services/iam/migrations/postgres/000077_add_history_cleared_at.down.sql @@ -0,0 +1,2 @@ +-- 000077: Rollback history_cleared_at from chat_participant. +ALTER TABLE chat_participant DROP COLUMN IF EXISTS history_cleared_at; diff --git a/services/iam/migrations/postgres/000077_add_history_cleared_at.up.sql b/services/iam/migrations/postgres/000077_add_history_cleared_at.up.sql new file mode 100644 index 00000000..0cab2d76 --- /dev/null +++ b/services/iam/migrations/postgres/000077_add_history_cleared_at.up.sql @@ -0,0 +1,2 @@ +-- 000077: Add history_cleared_at to chat_participant for per-user "Clear History". +ALTER TABLE chat_participant ADD COLUMN IF NOT EXISTS history_cleared_at TIMESTAMPTZ; diff --git a/services/iam/migrations/postgres/000078_create_chat_attachment.down.sql b/services/iam/migrations/postgres/000078_create_chat_attachment.down.sql new file mode 100644 index 00000000..e960f77f --- /dev/null +++ b/services/iam/migrations/postgres/000078_create_chat_attachment.down.sql @@ -0,0 +1,4 @@ +-- 000078 down: Drop chat_attachment table and its indexes. +DROP INDEX IF EXISTS idx_chat_attachment_conv; +DROP INDEX IF EXISTS idx_chat_attachment_message; +DROP TABLE IF EXISTS chat_attachment; diff --git a/services/iam/migrations/postgres/000078_create_chat_attachment.up.sql b/services/iam/migrations/postgres/000078_create_chat_attachment.up.sql new file mode 100644 index 00000000..26d4ebb1 --- /dev/null +++ b/services/iam/migrations/postgres/000078_create_chat_attachment.up.sql @@ -0,0 +1,16 @@ +-- 000078: Create chat_attachment table for file/image attachments on chat messages. +CREATE TABLE IF NOT EXISTS chat_attachment ( + attachment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES chat_conversation(conversation_id), + message_id UUID REFERENCES chat_message(message_id), + uploader_user_id UUID NOT NULL, + file_name VARCHAR(255) NOT NULL, + file_url VARCHAR(500) NOT NULL, + content_type VARCHAR(100) NOT NULL, + file_size BIGINT NOT NULL, + thumbnail_url VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_chat_attachment_message ON chat_attachment(message_id); +CREATE INDEX IF NOT EXISTS idx_chat_attachment_conv ON chat_attachment(conversation_id);