commit c833ff147ae24cb459c646474ac2a71bdc11741f Author: l-ai dev Date: Sat Aug 8 19:16:23 2026 +0300 chore: initial chat proto contracts (topics, messages, subscribe, acknowledge) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec48f5c --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# l-ai-chat-proto + +Protobuf-контракты Live Chat (`l-ai-chat`). Репозиторий хранит и `.proto`, и сгенерированный Go-код (как остальные proto-модули проекта). + +## Содержимое + +- `chat.proto` — единый контракт `ChatService` (topics, post/get/history, subscribe, acknowledge) + `Message`/`Topic` + enums `AuthorType`/`MessageSource`. +- `chat.pb.go`, `chat_grpc.pb.go` — сгенерированный код, закоммичен. + +## Регенерация + +Нужны `protoc` + `protoc-gen-go` + `protoc-gen-go-grpc` в PATH: + +```bat +generate_proto.bat +``` + +Сгенерированный код коммитится в этот репозиторий; сервисы подключают его как git submodule (см. `l-ai-chat/proto/l-ai-chat-proto`). + +## Соглашения + +- `package chat`, `option go_package = "git.be.kaien.ru/l-ai/l-ai-chat-proto/proto"`. +- Enum-значения префиксованы (`AUTHOR_TYPE_*`, `MESSAGE_SOURCE_*`) — избегаем C++-scoping коллизий. +- История пагинируется по `sequence` (`before_sequence`/`after_sequence`), offset-пагинации нет. diff --git a/chat.pb.go b/chat.pb.go new file mode 100644 index 0000000..3f3d31d --- /dev/null +++ b/chat.pb.go @@ -0,0 +1,1215 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: chat.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + 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) +) + +// AuthorType is the kind of identity a message author has. HUMAN covers +// internal users (the same author_id across Web and Telegram), AGENT covers +// l-ai agents, SYSTEM covers service-internal messages, INTEGRATION covers +// bridge/external systems. +type AuthorType int32 + +const ( + AuthorType_AUTHOR_TYPE_UNSPECIFIED AuthorType = 0 + AuthorType_AUTHOR_TYPE_HUMAN AuthorType = 1 + AuthorType_AUTHOR_TYPE_AGENT AuthorType = 2 + AuthorType_AUTHOR_TYPE_SYSTEM AuthorType = 3 + AuthorType_AUTHOR_TYPE_INTEGRATION AuthorType = 4 +) + +// Enum value maps for AuthorType. +var ( + AuthorType_name = map[int32]string{ + 0: "AUTHOR_TYPE_UNSPECIFIED", + 1: "AUTHOR_TYPE_HUMAN", + 2: "AUTHOR_TYPE_AGENT", + 3: "AUTHOR_TYPE_SYSTEM", + 4: "AUTHOR_TYPE_INTEGRATION", + } + AuthorType_value = map[string]int32{ + "AUTHOR_TYPE_UNSPECIFIED": 0, + "AUTHOR_TYPE_HUMAN": 1, + "AUTHOR_TYPE_AGENT": 2, + "AUTHOR_TYPE_SYSTEM": 3, + "AUTHOR_TYPE_INTEGRATION": 4, + } +) + +func (x AuthorType) Enum() *AuthorType { + p := new(AuthorType) + *p = x + return p +} + +func (x AuthorType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AuthorType) Descriptor() protoreflect.EnumDescriptor { + return file_chat_proto_enumTypes[0].Descriptor() +} + +func (AuthorType) Type() protoreflect.EnumType { + return &file_chat_proto_enumTypes[0] +} + +func (x AuthorType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AuthorType.Descriptor instead. +func (AuthorType) EnumDescriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{0} +} + +// MessageSource is the channel through which a message entered the system. +type MessageSource int32 + +const ( + MessageSource_MESSAGE_SOURCE_UNSPECIFIED MessageSource = 0 + MessageSource_MESSAGE_SOURCE_WEB MessageSource = 1 + MessageSource_MESSAGE_SOURCE_TELEGRAM MessageSource = 2 + MessageSource_MESSAGE_SOURCE_AGENT MessageSource = 3 + MessageSource_MESSAGE_SOURCE_SYSTEM MessageSource = 4 +) + +// Enum value maps for MessageSource. +var ( + MessageSource_name = map[int32]string{ + 0: "MESSAGE_SOURCE_UNSPECIFIED", + 1: "MESSAGE_SOURCE_WEB", + 2: "MESSAGE_SOURCE_TELEGRAM", + 3: "MESSAGE_SOURCE_AGENT", + 4: "MESSAGE_SOURCE_SYSTEM", + } + MessageSource_value = map[string]int32{ + "MESSAGE_SOURCE_UNSPECIFIED": 0, + "MESSAGE_SOURCE_WEB": 1, + "MESSAGE_SOURCE_TELEGRAM": 2, + "MESSAGE_SOURCE_AGENT": 3, + "MESSAGE_SOURCE_SYSTEM": 4, + } +) + +func (x MessageSource) Enum() *MessageSource { + p := new(MessageSource) + *p = x + return p +} + +func (x MessageSource) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MessageSource) Descriptor() protoreflect.EnumDescriptor { + return file_chat_proto_enumTypes[1].Descriptor() +} + +func (MessageSource) Type() protoreflect.EnumType { + return &file_chat_proto_enumTypes[1] +} + +func (x MessageSource) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MessageSource.Descriptor instead. +func (MessageSource) EnumDescriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{1} +} + +type Topic struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` // stable, unique, e.g. "security" + DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // e.g. "Security" + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Topic) Reset() { + *x = Topic{} + mi := &file_chat_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Topic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Topic) ProtoMessage() {} + +func (x *Topic) ProtoReflect() protoreflect.Message { + mi := &file_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 Topic.ProtoReflect.Descriptor instead. +func (*Topic) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{0} +} + +func (x *Topic) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Topic) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *Topic) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *Topic) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID, canonical message id + Sequence int64 `protobuf:"varint,2,opt,name=sequence,proto3" json:"sequence,omitempty"` // server-assigned monotonic sequence (ordering + durable cursor) + TopicId string `protobuf:"bytes,3,opt,name=topic_id,json=topicId,proto3" json:"topic_id,omitempty"` // UUID + AuthorId string `protobuf:"bytes,4,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` // stable identity of the author (UUID) + AuthorType AuthorType `protobuf:"varint,5,opt,name=author_type,json=authorType,proto3,enum=chat.AuthorType" json:"author_type,omitempty"` + Source MessageSource `protobuf:"varint,6,opt,name=source,proto3,enum=chat.MessageSource" json:"source,omitempty"` + Content string `protobuf:"bytes,7,opt,name=content,proto3" json:"content,omitempty"` + ReplyToMessageId *string `protobuf:"bytes,8,opt,name=reply_to_message_id,json=replyToMessageId,proto3,oneof" json:"reply_to_message_id,omitempty"` // parent message when this is a reply + RootMessageId string `protobuf:"bytes,9,opt,name=root_message_id,json=rootMessageId,proto3" json:"root_message_id,omitempty"` // root of the reply chain (== id for non-replies) + ReplyDepth int32 `protobuf:"varint,10,opt,name=reply_depth,json=replyDepth,proto3" json:"reply_depth,omitempty"` // 0 for non-replies, parent depth + 1 for replies; hard max 20 + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Message) Reset() { + *x = Message{} + mi := &file_chat_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_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 Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{1} +} + +func (x *Message) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Message) GetSequence() int64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *Message) GetTopicId() string { + if x != nil { + return x.TopicId + } + return "" +} + +func (x *Message) GetAuthorId() string { + if x != nil { + return x.AuthorId + } + return "" +} + +func (x *Message) GetAuthorType() AuthorType { + if x != nil { + return x.AuthorType + } + return AuthorType_AUTHOR_TYPE_UNSPECIFIED +} + +func (x *Message) GetSource() MessageSource { + if x != nil { + return x.Source + } + return MessageSource_MESSAGE_SOURCE_UNSPECIFIED +} + +func (x *Message) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Message) GetReplyToMessageId() string { + if x != nil && x.ReplyToMessageId != nil { + return *x.ReplyToMessageId + } + return "" +} + +func (x *Message) GetRootMessageId() string { + if x != nil { + return x.RootMessageId + } + return "" +} + +func (x *Message) GetReplyDepth() int32 { + if x != nil { + return x.ReplyDepth + } + return 0 +} + +func (x *Message) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type ListTopicsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsRequest) Reset() { + *x = ListTopicsRequest{} + mi := &file_chat_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsRequest) ProtoMessage() {} + +func (x *ListTopicsRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 ListTopicsRequest.ProtoReflect.Descriptor instead. +func (*ListTopicsRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{2} +} + +type ListTopicsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Topics []*Topic `protobuf:"bytes,1,rep,name=topics,proto3" json:"topics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsResponse) Reset() { + *x = ListTopicsResponse{} + mi := &file_chat_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsResponse) ProtoMessage() {} + +func (x *ListTopicsResponse) ProtoReflect() protoreflect.Message { + mi := &file_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 ListTopicsResponse.ProtoReflect.Descriptor instead. +func (*ListTopicsResponse) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{3} +} + +func (x *ListTopicsResponse) GetTopics() []*Topic { + if x != nil { + return x.Topics + } + return nil +} + +type GetTopicRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selector: + // + // *GetTopicRequest_Id + // *GetTopicRequest_Slug + Selector isGetTopicRequest_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTopicRequest) Reset() { + *x = GetTopicRequest{} + mi := &file_chat_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTopicRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTopicRequest) ProtoMessage() {} + +func (x *GetTopicRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 GetTopicRequest.ProtoReflect.Descriptor instead. +func (*GetTopicRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{4} +} + +func (x *GetTopicRequest) GetSelector() isGetTopicRequest_Selector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *GetTopicRequest) GetId() string { + if x != nil { + if x, ok := x.Selector.(*GetTopicRequest_Id); ok { + return x.Id + } + } + return "" +} + +func (x *GetTopicRequest) GetSlug() string { + if x != nil { + if x, ok := x.Selector.(*GetTopicRequest_Slug); ok { + return x.Slug + } + } + return "" +} + +type isGetTopicRequest_Selector interface { + isGetTopicRequest_Selector() +} + +type GetTopicRequest_Id struct { + Id string `protobuf:"bytes,1,opt,name=id,proto3,oneof"` +} + +type GetTopicRequest_Slug struct { + Slug string `protobuf:"bytes,2,opt,name=slug,proto3,oneof"` +} + +func (*GetTopicRequest_Id) isGetTopicRequest_Selector() {} + +func (*GetTopicRequest_Slug) isGetTopicRequest_Selector() {} + +type CreateTopicRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTopicRequest) Reset() { + *x = CreateTopicRequest{} + mi := &file_chat_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTopicRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTopicRequest) ProtoMessage() {} + +func (x *CreateTopicRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 CreateTopicRequest.ProtoReflect.Descriptor instead. +func (*CreateTopicRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateTopicRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *CreateTopicRequest) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +type PostMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TopicId string `protobuf:"bytes,1,opt,name=topic_id,json=topicId,proto3" json:"topic_id,omitempty"` + AuthorId string `protobuf:"bytes,2,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` + AuthorType AuthorType `protobuf:"varint,3,opt,name=author_type,json=authorType,proto3,enum=chat.AuthorType" json:"author_type,omitempty"` + Source MessageSource `protobuf:"varint,4,opt,name=source,proto3,enum=chat.MessageSource" json:"source,omitempty"` + Content string `protobuf:"bytes,5,opt,name=content,proto3" json:"content,omitempty"` + ReplyToMessageId *string `protobuf:"bytes,6,opt,name=reply_to_message_id,json=replyToMessageId,proto3,oneof" json:"reply_to_message_id,omitempty"` + // Idempotency key: retrying PostMessage with the same author_id and key + // returns the original message instead of creating a duplicate. + IdempotencyKey string `protobuf:"bytes,7,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostMessageRequest) Reset() { + *x = PostMessageRequest{} + mi := &file_chat_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostMessageRequest) ProtoMessage() {} + +func (x *PostMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 PostMessageRequest.ProtoReflect.Descriptor instead. +func (*PostMessageRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{6} +} + +func (x *PostMessageRequest) GetTopicId() string { + if x != nil { + return x.TopicId + } + return "" +} + +func (x *PostMessageRequest) GetAuthorId() string { + if x != nil { + return x.AuthorId + } + return "" +} + +func (x *PostMessageRequest) GetAuthorType() AuthorType { + if x != nil { + return x.AuthorType + } + return AuthorType_AUTHOR_TYPE_UNSPECIFIED +} + +func (x *PostMessageRequest) GetSource() MessageSource { + if x != nil { + return x.Source + } + return MessageSource_MESSAGE_SOURCE_UNSPECIFIED +} + +func (x *PostMessageRequest) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *PostMessageRequest) GetReplyToMessageId() string { + if x != nil && x.ReplyToMessageId != nil { + return *x.ReplyToMessageId + } + return "" +} + +func (x *PostMessageRequest) GetIdempotencyKey() string { + if x != nil { + return x.IdempotencyKey + } + return "" +} + +type PostMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message *Message `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Duplicated bool `protobuf:"varint,2,opt,name=duplicated,proto3" json:"duplicated,omitempty"` // true when an idempotent replay returned an existing message + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostMessageResponse) Reset() { + *x = PostMessageResponse{} + mi := &file_chat_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostMessageResponse) ProtoMessage() {} + +func (x *PostMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_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 PostMessageResponse.ProtoReflect.Descriptor instead. +func (*PostMessageResponse) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{7} +} + +func (x *PostMessageResponse) GetMessage() *Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *PostMessageResponse) GetDuplicated() bool { + if x != nil { + return x.Duplicated + } + return false +} + +type GetMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + MessageId string `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMessageRequest) Reset() { + *x = GetMessageRequest{} + mi := &file_chat_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMessageRequest) ProtoMessage() {} + +func (x *GetMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 GetMessageRequest.ProtoReflect.Descriptor instead. +func (*GetMessageRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{8} +} + +func (x *GetMessageRequest) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +type HistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TopicId string `protobuf:"bytes,1,opt,name=topic_id,json=topicId,proto3" json:"topic_id,omitempty"` + // Backward paging: return messages with sequence < before_sequence, + // newest first. Mutually exclusive with after_sequence. + BeforeSequence int64 `protobuf:"varint,2,opt,name=before_sequence,json=beforeSequence,proto3" json:"before_sequence,omitempty"` + // Forward paging: return messages with sequence > after_sequence, + // oldest first. Mutually exclusive with before_sequence. + AfterSequence int64 `protobuf:"varint,3,opt,name=after_sequence,json=afterSequence,proto3" json:"after_sequence,omitempty"` + // Maximum number of messages to return (server may cap it). + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HistoryRequest) Reset() { + *x = HistoryRequest{} + mi := &file_chat_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HistoryRequest) ProtoMessage() {} + +func (x *HistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 HistoryRequest.ProtoReflect.Descriptor instead. +func (*HistoryRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{9} +} + +func (x *HistoryRequest) GetTopicId() string { + if x != nil { + return x.TopicId + } + return "" +} + +func (x *HistoryRequest) GetBeforeSequence() int64 { + if x != nil { + return x.BeforeSequence + } + return 0 +} + +func (x *HistoryRequest) GetAfterSequence() int64 { + if x != nil { + return x.AfterSequence + } + return 0 +} + +func (x *HistoryRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type HistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Messages []*Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + // true when more messages exist in the requested direction. + HasMore bool `protobuf:"varint,2,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HistoryResponse) Reset() { + *x = HistoryResponse{} + mi := &file_chat_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HistoryResponse) ProtoMessage() {} + +func (x *HistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_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 HistoryResponse.ProtoReflect.Descriptor instead. +func (*HistoryResponse) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{10} +} + +func (x *HistoryResponse) GetMessages() []*Message { + if x != nil { + return x.Messages + } + return nil +} + +func (x *HistoryResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type SubscribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConsumerId string `protobuf:"bytes,1,opt,name=consumer_id,json=consumerId,proto3" json:"consumer_id,omitempty"` // stable identity of the consumer/agent + TopicId string `protobuf:"bytes,2,opt,name=topic_id,json=topicId,proto3" json:"topic_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeRequest) Reset() { + *x = SubscribeRequest{} + mi := &file_chat_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeRequest) ProtoMessage() {} + +func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 SubscribeRequest.ProtoReflect.Descriptor instead. +func (*SubscribeRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{11} +} + +func (x *SubscribeRequest) GetConsumerId() string { + if x != nil { + return x.ConsumerId + } + return "" +} + +func (x *SubscribeRequest) GetTopicId() string { + if x != nil { + return x.TopicId + } + return "" +} + +type AcknowledgeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConsumerId string `protobuf:"bytes,1,opt,name=consumer_id,json=consumerId,proto3" json:"consumer_id,omitempty"` + TopicId string `protobuf:"bytes,2,opt,name=topic_id,json=topicId,proto3" json:"topic_id,omitempty"` + LastAckSequence int64 `protobuf:"varint,3,opt,name=last_ack_sequence,json=lastAckSequence,proto3" json:"last_ack_sequence,omitempty"` // highest sequence processed by the consumer + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AcknowledgeRequest) Reset() { + *x = AcknowledgeRequest{} + mi := &file_chat_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AcknowledgeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AcknowledgeRequest) ProtoMessage() {} + +func (x *AcknowledgeRequest) ProtoReflect() protoreflect.Message { + mi := &file_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 AcknowledgeRequest.ProtoReflect.Descriptor instead. +func (*AcknowledgeRequest) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{12} +} + +func (x *AcknowledgeRequest) GetConsumerId() string { + if x != nil { + return x.ConsumerId + } + return "" +} + +func (x *AcknowledgeRequest) GetTopicId() string { + if x != nil { + return x.TopicId + } + return "" +} + +func (x *AcknowledgeRequest) GetLastAckSequence() int64 { + if x != nil { + return x.LastAckSequence + } + return 0 +} + +type AcknowledgeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + LastAckSequence int64 `protobuf:"varint,1,opt,name=last_ack_sequence,json=lastAckSequence,proto3" json:"last_ack_sequence,omitempty"` // the cursor as stored (never lower than before) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AcknowledgeResponse) Reset() { + *x = AcknowledgeResponse{} + mi := &file_chat_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AcknowledgeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AcknowledgeResponse) ProtoMessage() {} + +func (x *AcknowledgeResponse) ProtoReflect() protoreflect.Message { + mi := &file_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 AcknowledgeResponse.ProtoReflect.Descriptor instead. +func (*AcknowledgeResponse) Descriptor() ([]byte, []int) { + return file_chat_proto_rawDescGZIP(), []int{13} +} + +func (x *AcknowledgeResponse) GetLastAckSequence() int64 { + if x != nil { + return x.LastAckSequence + } + return 0 +} + +var File_chat_proto protoreflect.FileDescriptor + +const file_chat_proto_rawDesc = "" + + "\n" + + "\n" + + "chat.proto\x12\x04chat\x1a\x1fgoogle/protobuf/timestamp.proto\"\x89\x01\n" + + "\x05Topic\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12!\n" + + "\fdisplay_name\x18\x03 \x01(\tR\vdisplayName\x129\n" + + "\n" + + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xb7\x03\n" + + "\aMessage\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + + "\bsequence\x18\x02 \x01(\x03R\bsequence\x12\x19\n" + + "\btopic_id\x18\x03 \x01(\tR\atopicId\x12\x1b\n" + + "\tauthor_id\x18\x04 \x01(\tR\bauthorId\x121\n" + + "\vauthor_type\x18\x05 \x01(\x0e2\x10.chat.AuthorTypeR\n" + + "authorType\x12+\n" + + "\x06source\x18\x06 \x01(\x0e2\x13.chat.MessageSourceR\x06source\x12\x18\n" + + "\acontent\x18\a \x01(\tR\acontent\x122\n" + + "\x13reply_to_message_id\x18\b \x01(\tH\x00R\x10replyToMessageId\x88\x01\x01\x12&\n" + + "\x0froot_message_id\x18\t \x01(\tR\rrootMessageId\x12\x1f\n" + + "\vreply_depth\x18\n" + + " \x01(\x05R\n" + + "replyDepth\x129\n" + + "\n" + + "created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAtB\x16\n" + + "\x14_reply_to_message_id\"\x13\n" + + "\x11ListTopicsRequest\"9\n" + + "\x12ListTopicsResponse\x12#\n" + + "\x06topics\x18\x01 \x03(\v2\v.chat.TopicR\x06topics\"E\n" + + "\x0fGetTopicRequest\x12\x10\n" + + "\x02id\x18\x01 \x01(\tH\x00R\x02id\x12\x14\n" + + "\x04slug\x18\x02 \x01(\tH\x00R\x04slugB\n" + + "\n" + + "\bselector\"K\n" + + "\x12CreateTopicRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\"\xbb\x02\n" + + "\x12PostMessageRequest\x12\x19\n" + + "\btopic_id\x18\x01 \x01(\tR\atopicId\x12\x1b\n" + + "\tauthor_id\x18\x02 \x01(\tR\bauthorId\x121\n" + + "\vauthor_type\x18\x03 \x01(\x0e2\x10.chat.AuthorTypeR\n" + + "authorType\x12+\n" + + "\x06source\x18\x04 \x01(\x0e2\x13.chat.MessageSourceR\x06source\x12\x18\n" + + "\acontent\x18\x05 \x01(\tR\acontent\x122\n" + + "\x13reply_to_message_id\x18\x06 \x01(\tH\x00R\x10replyToMessageId\x88\x01\x01\x12'\n" + + "\x0fidempotency_key\x18\a \x01(\tR\x0eidempotencyKeyB\x16\n" + + "\x14_reply_to_message_id\"^\n" + + "\x13PostMessageResponse\x12'\n" + + "\amessage\x18\x01 \x01(\v2\r.chat.MessageR\amessage\x12\x1e\n" + + "\n" + + "duplicated\x18\x02 \x01(\bR\n" + + "duplicated\"2\n" + + "\x11GetMessageRequest\x12\x1d\n" + + "\n" + + "message_id\x18\x01 \x01(\tR\tmessageId\"\x91\x01\n" + + "\x0eHistoryRequest\x12\x19\n" + + "\btopic_id\x18\x01 \x01(\tR\atopicId\x12'\n" + + "\x0fbefore_sequence\x18\x02 \x01(\x03R\x0ebeforeSequence\x12%\n" + + "\x0eafter_sequence\x18\x03 \x01(\x03R\rafterSequence\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x05R\x05limit\"W\n" + + "\x0fHistoryResponse\x12)\n" + + "\bmessages\x18\x01 \x03(\v2\r.chat.MessageR\bmessages\x12\x19\n" + + "\bhas_more\x18\x02 \x01(\bR\ahasMore\"N\n" + + "\x10SubscribeRequest\x12\x1f\n" + + "\vconsumer_id\x18\x01 \x01(\tR\n" + + "consumerId\x12\x19\n" + + "\btopic_id\x18\x02 \x01(\tR\atopicId\"|\n" + + "\x12AcknowledgeRequest\x12\x1f\n" + + "\vconsumer_id\x18\x01 \x01(\tR\n" + + "consumerId\x12\x19\n" + + "\btopic_id\x18\x02 \x01(\tR\atopicId\x12*\n" + + "\x11last_ack_sequence\x18\x03 \x01(\x03R\x0flastAckSequence\"A\n" + + "\x13AcknowledgeResponse\x12*\n" + + "\x11last_ack_sequence\x18\x01 \x01(\x03R\x0flastAckSequence*\x8c\x01\n" + + "\n" + + "AuthorType\x12\x1b\n" + + "\x17AUTHOR_TYPE_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11AUTHOR_TYPE_HUMAN\x10\x01\x12\x15\n" + + "\x11AUTHOR_TYPE_AGENT\x10\x02\x12\x16\n" + + "\x12AUTHOR_TYPE_SYSTEM\x10\x03\x12\x1b\n" + + "\x17AUTHOR_TYPE_INTEGRATION\x10\x04*\x99\x01\n" + + "\rMessageSource\x12\x1e\n" + + "\x1aMESSAGE_SOURCE_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12MESSAGE_SOURCE_WEB\x10\x01\x12\x1b\n" + + "\x17MESSAGE_SOURCE_TELEGRAM\x10\x02\x12\x18\n" + + "\x14MESSAGE_SOURCE_AGENT\x10\x03\x12\x19\n" + + "\x15MESSAGE_SOURCE_SYSTEM\x10\x042\xe0\x03\n" + + "\vChatService\x12?\n" + + "\n" + + "ListTopics\x12\x17.chat.ListTopicsRequest\x1a\x18.chat.ListTopicsResponse\x12.\n" + + "\bGetTopic\x12\x15.chat.GetTopicRequest\x1a\v.chat.Topic\x124\n" + + "\vCreateTopic\x12\x18.chat.CreateTopicRequest\x1a\v.chat.Topic\x12B\n" + + "\vPostMessage\x12\x18.chat.PostMessageRequest\x1a\x19.chat.PostMessageResponse\x124\n" + + "\n" + + "GetMessage\x12\x17.chat.GetMessageRequest\x1a\r.chat.Message\x126\n" + + "\aHistory\x12\x14.chat.HistoryRequest\x1a\x15.chat.HistoryResponse\x124\n" + + "\tSubscribe\x12\x16.chat.SubscribeRequest\x1a\r.chat.Message0\x01\x12B\n" + + "\vAcknowledge\x12\x18.chat.AcknowledgeRequest\x1a\x19.chat.AcknowledgeResponseB,Z*git.be.kaien.ru/l-ai/l-ai-chat-proto/protob\x06proto3" + +var ( + file_chat_proto_rawDescOnce sync.Once + file_chat_proto_rawDescData []byte +) + +func file_chat_proto_rawDescGZIP() []byte { + file_chat_proto_rawDescOnce.Do(func() { + file_chat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_chat_proto_rawDesc), len(file_chat_proto_rawDesc))) + }) + return file_chat_proto_rawDescData +} + +var file_chat_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_chat_proto_goTypes = []any{ + (AuthorType)(0), // 0: chat.AuthorType + (MessageSource)(0), // 1: chat.MessageSource + (*Topic)(nil), // 2: chat.Topic + (*Message)(nil), // 3: chat.Message + (*ListTopicsRequest)(nil), // 4: chat.ListTopicsRequest + (*ListTopicsResponse)(nil), // 5: chat.ListTopicsResponse + (*GetTopicRequest)(nil), // 6: chat.GetTopicRequest + (*CreateTopicRequest)(nil), // 7: chat.CreateTopicRequest + (*PostMessageRequest)(nil), // 8: chat.PostMessageRequest + (*PostMessageResponse)(nil), // 9: chat.PostMessageResponse + (*GetMessageRequest)(nil), // 10: chat.GetMessageRequest + (*HistoryRequest)(nil), // 11: chat.HistoryRequest + (*HistoryResponse)(nil), // 12: chat.HistoryResponse + (*SubscribeRequest)(nil), // 13: chat.SubscribeRequest + (*AcknowledgeRequest)(nil), // 14: chat.AcknowledgeRequest + (*AcknowledgeResponse)(nil), // 15: chat.AcknowledgeResponse + (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp +} +var file_chat_proto_depIdxs = []int32{ + 16, // 0: chat.Topic.created_at:type_name -> google.protobuf.Timestamp + 0, // 1: chat.Message.author_type:type_name -> chat.AuthorType + 1, // 2: chat.Message.source:type_name -> chat.MessageSource + 16, // 3: chat.Message.created_at:type_name -> google.protobuf.Timestamp + 2, // 4: chat.ListTopicsResponse.topics:type_name -> chat.Topic + 0, // 5: chat.PostMessageRequest.author_type:type_name -> chat.AuthorType + 1, // 6: chat.PostMessageRequest.source:type_name -> chat.MessageSource + 3, // 7: chat.PostMessageResponse.message:type_name -> chat.Message + 3, // 8: chat.HistoryResponse.messages:type_name -> chat.Message + 4, // 9: chat.ChatService.ListTopics:input_type -> chat.ListTopicsRequest + 6, // 10: chat.ChatService.GetTopic:input_type -> chat.GetTopicRequest + 7, // 11: chat.ChatService.CreateTopic:input_type -> chat.CreateTopicRequest + 8, // 12: chat.ChatService.PostMessage:input_type -> chat.PostMessageRequest + 10, // 13: chat.ChatService.GetMessage:input_type -> chat.GetMessageRequest + 11, // 14: chat.ChatService.History:input_type -> chat.HistoryRequest + 13, // 15: chat.ChatService.Subscribe:input_type -> chat.SubscribeRequest + 14, // 16: chat.ChatService.Acknowledge:input_type -> chat.AcknowledgeRequest + 5, // 17: chat.ChatService.ListTopics:output_type -> chat.ListTopicsResponse + 2, // 18: chat.ChatService.GetTopic:output_type -> chat.Topic + 2, // 19: chat.ChatService.CreateTopic:output_type -> chat.Topic + 9, // 20: chat.ChatService.PostMessage:output_type -> chat.PostMessageResponse + 3, // 21: chat.ChatService.GetMessage:output_type -> chat.Message + 12, // 22: chat.ChatService.History:output_type -> chat.HistoryResponse + 3, // 23: chat.ChatService.Subscribe:output_type -> chat.Message + 15, // 24: chat.ChatService.Acknowledge:output_type -> chat.AcknowledgeResponse + 17, // [17:25] is the sub-list for method output_type + 9, // [9:17] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_chat_proto_init() } +func file_chat_proto_init() { + if File_chat_proto != nil { + return + } + file_chat_proto_msgTypes[1].OneofWrappers = []any{} + file_chat_proto_msgTypes[4].OneofWrappers = []any{ + (*GetTopicRequest_Id)(nil), + (*GetTopicRequest_Slug)(nil), + } + file_chat_proto_msgTypes[6].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_chat_proto_rawDesc), len(file_chat_proto_rawDesc)), + NumEnums: 2, + NumMessages: 14, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_chat_proto_goTypes, + DependencyIndexes: file_chat_proto_depIdxs, + EnumInfos: file_chat_proto_enumTypes, + MessageInfos: file_chat_proto_msgTypes, + }.Build() + File_chat_proto = out.File + file_chat_proto_goTypes = nil + file_chat_proto_depIdxs = nil +} diff --git a/chat.proto b/chat.proto new file mode 100644 index 0000000..cf42254 --- /dev/null +++ b/chat.proto @@ -0,0 +1,157 @@ +syntax = "proto3"; + +package chat; + +option go_package = "git.be.kaien.ru/l-ai/l-ai-chat-proto/proto"; + +import "google/protobuf/timestamp.proto"; + +// ChatService is the canonical API of the l-ai-chat Live Chat service. +// Chat Service is the single source of truth for messages; clients (Web UI, +// Telegram bot, agents) send and read messages through it. +service ChatService { + // ListTopics returns all topics in stable (creation) order. + rpc ListTopics(ListTopicsRequest) returns (ListTopicsResponse); + + // GetTopic returns one topic by id or slug. + rpc GetTopic(GetTopicRequest) returns (Topic); + + // CreateTopic creates a new topic. Slug must be unique. + rpc CreateTopic(CreateTopicRequest) returns (Topic); + + // PostMessage appends a message to a topic. Repeated calls with the same + // (author_id, idempotency_key) must not create a second message; the + // original message is returned with duplicated=true instead. + rpc PostMessage(PostMessageRequest) returns (PostMessageResponse); + + // GetMessage returns a single message by id. + rpc GetMessage(GetMessageRequest) returns (Message); + + // History returns messages of a topic, paginated by sequence (no offset + // pagination). Use before_sequence for newest-first backward paging or + // after_sequence for oldest-first forward paging; at most one may be set. + rpc History(HistoryRequest) returns (HistoryResponse); + + // Subscribe streams new messages of a topic to a consumer. The consumer is + // expected to Acknowledge the messages it processed (at-least-once). + // NOTE: stream delivery is implemented in a later stage. + rpc Subscribe(SubscribeRequest) returns (stream Message); + + // Acknowledge advances the durable delivery cursor of a consumer in a + // topic. Cursors are monotonic: the stored value never moves backwards. + rpc Acknowledge(AcknowledgeRequest) returns (AcknowledgeResponse); +} + +// AuthorType is the kind of identity a message author has. HUMAN covers +// internal users (the same author_id across Web and Telegram), AGENT covers +// l-ai agents, SYSTEM covers service-internal messages, INTEGRATION covers +// bridge/external systems. +enum AuthorType { + AUTHOR_TYPE_UNSPECIFIED = 0; + AUTHOR_TYPE_HUMAN = 1; + AUTHOR_TYPE_AGENT = 2; + AUTHOR_TYPE_SYSTEM = 3; + AUTHOR_TYPE_INTEGRATION = 4; +} + +// MessageSource is the channel through which a message entered the system. +enum MessageSource { + MESSAGE_SOURCE_UNSPECIFIED = 0; + MESSAGE_SOURCE_WEB = 1; + MESSAGE_SOURCE_TELEGRAM = 2; + MESSAGE_SOURCE_AGENT = 3; + MESSAGE_SOURCE_SYSTEM = 4; +} + +message Topic { + string id = 1; // UUID + string slug = 2; // stable, unique, e.g. "security" + string display_name = 3; // e.g. "Security" + google.protobuf.Timestamp created_at = 4; +} + +message Message { + string id = 1; // UUID, canonical message id + int64 sequence = 2; // server-assigned monotonic sequence (ordering + durable cursor) + string topic_id = 3; // UUID + string author_id = 4; // stable identity of the author (UUID) + AuthorType author_type = 5; + MessageSource source = 6; + string content = 7; + optional string reply_to_message_id = 8; // parent message when this is a reply + string root_message_id = 9; // root of the reply chain (== id for non-replies) + int32 reply_depth = 10; // 0 for non-replies, parent depth + 1 for replies; hard max 20 + google.protobuf.Timestamp created_at = 11; +} + +message ListTopicsRequest {} + +message ListTopicsResponse { + repeated Topic topics = 1; +} + +message GetTopicRequest { + oneof selector { + string id = 1; + string slug = 2; + } +} + +message CreateTopicRequest { + string slug = 1; + string display_name = 2; +} + +message PostMessageRequest { + string topic_id = 1; + string author_id = 2; + AuthorType author_type = 3; + MessageSource source = 4; + string content = 5; + optional string reply_to_message_id = 6; + // Idempotency key: retrying PostMessage with the same author_id and key + // returns the original message instead of creating a duplicate. + string idempotency_key = 7; +} + +message PostMessageResponse { + Message message = 1; + bool duplicated = 2; // true when an idempotent replay returned an existing message +} + +message GetMessageRequest { + string message_id = 1; +} + +message HistoryRequest { + string topic_id = 1; + // Backward paging: return messages with sequence < before_sequence, + // newest first. Mutually exclusive with after_sequence. + int64 before_sequence = 2; + // Forward paging: return messages with sequence > after_sequence, + // oldest first. Mutually exclusive with before_sequence. + int64 after_sequence = 3; + // Maximum number of messages to return (server may cap it). + int32 limit = 4; +} + +message HistoryResponse { + repeated Message messages = 1; + // true when more messages exist in the requested direction. + bool has_more = 2; +} + +message SubscribeRequest { + string consumer_id = 1; // stable identity of the consumer/agent + string topic_id = 2; +} + +message AcknowledgeRequest { + string consumer_id = 1; + string topic_id = 2; + int64 last_ack_sequence = 3; // highest sequence processed by the consumer +} + +message AcknowledgeResponse { + int64 last_ack_sequence = 1; // the cursor as stored (never lower than before) +} diff --git a/chat_grpc.pb.go b/chat_grpc.pb.go new file mode 100644 index 0000000..7c2634e --- /dev/null +++ b/chat_grpc.pb.go @@ -0,0 +1,429 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.35.1 +// source: chat.proto + +package proto + +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_ListTopics_FullMethodName = "/chat.ChatService/ListTopics" + ChatService_GetTopic_FullMethodName = "/chat.ChatService/GetTopic" + ChatService_CreateTopic_FullMethodName = "/chat.ChatService/CreateTopic" + ChatService_PostMessage_FullMethodName = "/chat.ChatService/PostMessage" + ChatService_GetMessage_FullMethodName = "/chat.ChatService/GetMessage" + ChatService_History_FullMethodName = "/chat.ChatService/History" + ChatService_Subscribe_FullMethodName = "/chat.ChatService/Subscribe" + ChatService_Acknowledge_FullMethodName = "/chat.ChatService/Acknowledge" +) + +// 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 is the canonical API of the l-ai-chat Live Chat service. +// Chat Service is the single source of truth for messages; clients (Web UI, +// Telegram bot, agents) send and read messages through it. +type ChatServiceClient interface { + // ListTopics returns all topics in stable (creation) order. + ListTopics(ctx context.Context, in *ListTopicsRequest, opts ...grpc.CallOption) (*ListTopicsResponse, error) + // GetTopic returns one topic by id or slug. + GetTopic(ctx context.Context, in *GetTopicRequest, opts ...grpc.CallOption) (*Topic, error) + // CreateTopic creates a new topic. Slug must be unique. + CreateTopic(ctx context.Context, in *CreateTopicRequest, opts ...grpc.CallOption) (*Topic, error) + // PostMessage appends a message to a topic. Repeated calls with the same + // (author_id, idempotency_key) must not create a second message; the + // original message is returned with duplicated=true instead. + PostMessage(ctx context.Context, in *PostMessageRequest, opts ...grpc.CallOption) (*PostMessageResponse, error) + // GetMessage returns a single message by id. + GetMessage(ctx context.Context, in *GetMessageRequest, opts ...grpc.CallOption) (*Message, error) + // History returns messages of a topic, paginated by sequence (no offset + // pagination). Use before_sequence for newest-first backward paging or + // after_sequence for oldest-first forward paging; at most one may be set. + History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) + // Subscribe streams new messages of a topic to a consumer. The consumer is + // expected to Acknowledge the messages it processed (at-least-once). + // NOTE: stream delivery is implemented in a later stage. + Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Message], error) + // Acknowledge advances the durable delivery cursor of a consumer in a + // topic. Cursors are monotonic: the stored value never moves backwards. + Acknowledge(ctx context.Context, in *AcknowledgeRequest, opts ...grpc.CallOption) (*AcknowledgeResponse, error) +} + +type chatServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewChatServiceClient(cc grpc.ClientConnInterface) ChatServiceClient { + return &chatServiceClient{cc} +} + +func (c *chatServiceClient) ListTopics(ctx context.Context, in *ListTopicsRequest, opts ...grpc.CallOption) (*ListTopicsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTopicsResponse) + err := c.cc.Invoke(ctx, ChatService_ListTopics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) GetTopic(ctx context.Context, in *GetTopicRequest, opts ...grpc.CallOption) (*Topic, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Topic) + err := c.cc.Invoke(ctx, ChatService_GetTopic_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) CreateTopic(ctx context.Context, in *CreateTopicRequest, opts ...grpc.CallOption) (*Topic, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Topic) + err := c.cc.Invoke(ctx, ChatService_CreateTopic_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) PostMessage(ctx context.Context, in *PostMessageRequest, opts ...grpc.CallOption) (*PostMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PostMessageResponse) + err := c.cc.Invoke(ctx, ChatService_PostMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) GetMessage(ctx context.Context, in *GetMessageRequest, opts ...grpc.CallOption) (*Message, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Message) + err := c.cc.Invoke(ctx, ChatService_GetMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HistoryResponse) + err := c.cc.Invoke(ctx, ChatService_History_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chatServiceClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Message], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[0], ChatService_Subscribe_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SubscribeRequest, Message]{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_SubscribeClient = grpc.ServerStreamingClient[Message] + +func (c *chatServiceClient) Acknowledge(ctx context.Context, in *AcknowledgeRequest, opts ...grpc.CallOption) (*AcknowledgeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AcknowledgeResponse) + err := c.cc.Invoke(ctx, ChatService_Acknowledge_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 is the canonical API of the l-ai-chat Live Chat service. +// Chat Service is the single source of truth for messages; clients (Web UI, +// Telegram bot, agents) send and read messages through it. +type ChatServiceServer interface { + // ListTopics returns all topics in stable (creation) order. + ListTopics(context.Context, *ListTopicsRequest) (*ListTopicsResponse, error) + // GetTopic returns one topic by id or slug. + GetTopic(context.Context, *GetTopicRequest) (*Topic, error) + // CreateTopic creates a new topic. Slug must be unique. + CreateTopic(context.Context, *CreateTopicRequest) (*Topic, error) + // PostMessage appends a message to a topic. Repeated calls with the same + // (author_id, idempotency_key) must not create a second message; the + // original message is returned with duplicated=true instead. + PostMessage(context.Context, *PostMessageRequest) (*PostMessageResponse, error) + // GetMessage returns a single message by id. + GetMessage(context.Context, *GetMessageRequest) (*Message, error) + // History returns messages of a topic, paginated by sequence (no offset + // pagination). Use before_sequence for newest-first backward paging or + // after_sequence for oldest-first forward paging; at most one may be set. + History(context.Context, *HistoryRequest) (*HistoryResponse, error) + // Subscribe streams new messages of a topic to a consumer. The consumer is + // expected to Acknowledge the messages it processed (at-least-once). + // NOTE: stream delivery is implemented in a later stage. + Subscribe(*SubscribeRequest, grpc.ServerStreamingServer[Message]) error + // Acknowledge advances the durable delivery cursor of a consumer in a + // topic. Cursors are monotonic: the stored value never moves backwards. + Acknowledge(context.Context, *AcknowledgeRequest) (*AcknowledgeResponse, 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) ListTopics(context.Context, *ListTopicsRequest) (*ListTopicsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTopics not implemented") +} +func (UnimplementedChatServiceServer) GetTopic(context.Context, *GetTopicRequest) (*Topic, error) { + return nil, status.Error(codes.Unimplemented, "method GetTopic not implemented") +} +func (UnimplementedChatServiceServer) CreateTopic(context.Context, *CreateTopicRequest) (*Topic, error) { + return nil, status.Error(codes.Unimplemented, "method CreateTopic not implemented") +} +func (UnimplementedChatServiceServer) PostMessage(context.Context, *PostMessageRequest) (*PostMessageResponse, error) { + return nil, status.Error(codes.Unimplemented, "method PostMessage not implemented") +} +func (UnimplementedChatServiceServer) GetMessage(context.Context, *GetMessageRequest) (*Message, error) { + return nil, status.Error(codes.Unimplemented, "method GetMessage not implemented") +} +func (UnimplementedChatServiceServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method History not implemented") +} +func (UnimplementedChatServiceServer) Subscribe(*SubscribeRequest, grpc.ServerStreamingServer[Message]) error { + return status.Error(codes.Unimplemented, "method Subscribe not implemented") +} +func (UnimplementedChatServiceServer) Acknowledge(context.Context, *AcknowledgeRequest) (*AcknowledgeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Acknowledge 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_ListTopics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTopicsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).ListTopics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_ListTopics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).ListTopics(ctx, req.(*ListTopicsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_GetTopic_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTopicRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).GetTopic(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_GetTopic_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).GetTopic(ctx, req.(*GetTopicRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_CreateTopic_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTopicRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).CreateTopic(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_CreateTopic_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).CreateTopic(ctx, req.(*CreateTopicRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_PostMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PostMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).PostMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_PostMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).PostMessage(ctx, req.(*PostMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_GetMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).GetMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_GetMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).GetMessage(ctx, req.(*GetMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).History(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_History_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).History(ctx, req.(*HistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChatService_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ChatServiceServer).Subscribe(m, &grpc.GenericServerStream[SubscribeRequest, Message]{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_SubscribeServer = grpc.ServerStreamingServer[Message] + +func _ChatService_Acknowledge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AcknowledgeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChatServiceServer).Acknowledge(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChatService_Acknowledge_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChatServiceServer).Acknowledge(ctx, req.(*AcknowledgeRequest)) + } + 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: "chat.ChatService", + HandlerType: (*ChatServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListTopics", + Handler: _ChatService_ListTopics_Handler, + }, + { + MethodName: "GetTopic", + Handler: _ChatService_GetTopic_Handler, + }, + { + MethodName: "CreateTopic", + Handler: _ChatService_CreateTopic_Handler, + }, + { + MethodName: "PostMessage", + Handler: _ChatService_PostMessage_Handler, + }, + { + MethodName: "GetMessage", + Handler: _ChatService_GetMessage_Handler, + }, + { + MethodName: "History", + Handler: _ChatService_History_Handler, + }, + { + MethodName: "Acknowledge", + Handler: _ChatService_Acknowledge_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Subscribe", + Handler: _ChatService_Subscribe_Handler, + ServerStreams: true, + }, + }, + Metadata: "chat.proto", +} diff --git a/generate_proto.bat b/generate_proto.bat new file mode 100644 index 0000000..b267b7c --- /dev/null +++ b/generate_proto.bat @@ -0,0 +1,6 @@ +@echo off +echo Generating gRPC code from chat.proto... +protoc --go_out=. --go_opt=paths=source_relative ^ + --go-grpc_out=. --go-grpc_opt=paths=source_relative ^ + chat.proto +echo Done!