Skip to content

Commit 288e78c

Browse files
authored
feat(chat): add Chat service layer (#870)
* chore(protos): update flipcash protobuf definitions New ChatId message, ChatUpdate event type for real-time chat streaming, and objc_class_prefix additions on contact protos. Signed-off-by: Brandon McAnsh <git@bmcreations.dev> * feat(events): add EventStreaming service layer Bidirectional event stream Api, Service, Repository, and Controller plus shared chat domain models and proto ↔ domain extensions. Domain models: ChatId, ChatMessage, MessageContent, MessagePointer, TypingNotification, ChatType, ChatMember, ChatMetadata, MetadataUpdate, ChatUpdate. Uses openBidirectionalStream from OCP bidi infrastructure with ping/pong keepalive. EventStreamingController exposes a SharedFlow of ChatUpdate for downstream consumers. Signed-off-by: Brandon McAnsh <git@bmcreations.dev> * feat(chat): add Chat service layer Unary RPCs for GetChat and GetDmChatFeed — Api, Service, ChatMetadataMapper, Repository, and Controller. Introduces ChatFeedPage domain model, GetChatError and GetDmChatFeedError sealed classes, and Hilt wiring. Signed-off-by: Brandon McAnsh <git@bmcreations.dev> * feat(messaging): add ChatMessaging service layer All 6 messaging RPCs — GetMessage, GetMessages, GetMessagesByIds, SendMessage, AdvancePointer, NotifyIsTyping — Api, Service, Repository, and Controller. Introduces ClientMessageId domain model, SendMessageError, GetMessageError, GetMessagesError, AdvancePointerError, and NotifyIsTypingError sealed classes, and Hilt wiring. Signed-off-by: Brandon McAnsh <git@bmcreations.dev> --------- Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent e2d087e commit 288e78c

47 files changed

Lines changed: 2771 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
syntax = "proto3";
2+
3+
package flipcash.chat.v1;
4+
5+
option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/chat/v1;chatpb";
6+
option java_package = "com.codeinc.flipcash.gen.chat.v1";
7+
option objc_class_prefix = "FPBChatV1";
8+
9+
import "chat/v1/model.proto";
10+
import "common/v1/common.proto";
11+
import "validate/validate.proto";
12+
13+
service Chat {
14+
// GetChat returns the metadata for a specific chat
15+
rpc GetChat(GetChatRequest) returns (GetChatResponse);
16+
17+
// GetDmChatFeed gets the set of DM chats for an owner account using
18+
// a paged API, ordered by last activity with the most recent first.
19+
//
20+
// Chats are ordered by a mutable key (last_activity), so pagination alone
21+
// cannot guarantee a complete read: a chat can receive new activity and
22+
// move into a region the client has already paged past. To get the full
23+
// list, the client MUST combine this RPC with the event stream:
24+
//
25+
// 1. Open the event stream to receive ChatUpdate and begin buffering updates
26+
// BEFORE the first GetDmChatFeed call. This ordering is the contract that
27+
// closes the gap; subscribing after pagination starts can drop chats.
28+
// 2. Page through GetDmChatFeed to exhaustion (until has_more is false),
29+
// always echoing back the paging token returned by the prior response.
30+
// All pages are served against a single snapshot pinned by that token,
31+
// so the set is read consistently.
32+
// 3. Merge the buffered and ongoing stream updates onto the paginated
33+
// set. Any chat whose activity changed after the snapshot watermark
34+
// is delivered via the stream rather than via pagination.
35+
//
36+
// Read together, pagination guarantees the set (every chat exactly once)
37+
// and the stream guarantees freshness and ordering. The local last_activity
38+
// sort is maintained by the client from the stream after the initial read.
39+
rpc GetDmChatFeed(GetDmChatFeedRequest) returns (GetDmChatFeedResponse);
40+
}
41+
42+
message GetChatRequest {
43+
common.v1.ChatId chat_id = 1;
44+
45+
common.v1.Auth auth = 10;
46+
}
47+
48+
message GetChatResponse {
49+
Result result = 1;
50+
enum Result {
51+
OK = 0;
52+
DENIED = 1;
53+
NOT_FOUND = 2;
54+
}
55+
56+
Metadata metadata = 2;
57+
}
58+
59+
message GetDmChatFeedRequest {
60+
// QueryOptions controls page_size. Ordering is fixed to most-recent
61+
// activity first and is not client-selectable.
62+
//
63+
// Leave query_options.paging_token unset on the first request: the server
64+
// mints a token that pins a new snapshot and returns it in the response. On
65+
// every subsequent request, set query_options.paging_token to the
66+
// paging_token from the most recent response to advance within the same
67+
// snapshot. The token is opaque and server-generated; do not construct it.
68+
common.v1.QueryOptions query_options = 1;
69+
70+
common.v1.Auth auth = 10 [(validate.rules).message.required = true];
71+
}
72+
73+
message GetDmChatFeedResponse {
74+
Result result = 1;
75+
enum Result {
76+
OK = 0;
77+
DENIED = 1;
78+
NOT_FOUND = 2;
79+
}
80+
81+
repeated Metadata chats = 2 [(validate.rules).repeated = {
82+
min_items: 0
83+
max_items: 100
84+
}];
85+
86+
// PagingToken is the server-generated token for this paginated read. On the
87+
// first response it pins a new snapshot; on later responses it carries the
88+
// advanced cursor over (last_activity, chat_id). The client MUST send the
89+
// most recent value back in query_options.paging_token on the next
90+
// GetDmChatFeedRequest. Set when result is OK.
91+
common.v1.PagingToken paging_token = 3;
92+
93+
// HasMore indicates whether further pages remain in this snapshot. When
94+
// false, the paginated set has been fully read; the complete chat list is
95+
// this set reconciled with the event stream (see GetDmChatFeed). When true, the
96+
// client should issue another GetDmChatFeedRequest with the returned
97+
// paging_token.
98+
bool has_more = 4;
99+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
syntax = "proto3";
2+
3+
package flipcash.chat.v1;
4+
5+
option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/chat/v1;chatpb";
6+
option java_package = "com.codeinc.flipcash.gen.chat.v1";
7+
option objc_class_prefix = "FPBChatV1";
8+
9+
import "common/v1/common.proto";
10+
import "profile/v1/model.proto";
11+
import "messaging/v1/model.proto";
12+
import "validate/validate.proto";
13+
import "google/protobuf/timestamp.proto";
14+
15+
message Metadata {
16+
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];
17+
18+
// The type of chat
19+
ChatType type = 2 [(validate.rules).enum = {
20+
not_in: [0] // UNKNOWN
21+
}];
22+
enum ChatType {
23+
UNKNOWN = 0;
24+
DM = 1;
25+
}
26+
27+
// Members of this chat
28+
repeated Member members = 3;
29+
30+
// The last message in this chat
31+
messaging.v1.Message last_message = 4;
32+
33+
// The timestamp of the last activity in this chat
34+
google.protobuf.Timestamp last_activity = 5 [(validate.rules).timestamp.required = true];
35+
}
36+
37+
message Member {
38+
common.v1.UserId user_id = 1 [(validate.rules).message.required = true];
39+
40+
// The user profile for this member. It contains a subset of identifiers
41+
// that can be publicly viewed within the chat.
42+
profile.v1.UserProfile user_profile = 2 [(validate.rules).message.required = true];
43+
44+
// Chat message state for this member.
45+
//
46+
// If set, the list may contain DELIVERED and READ pointers. SENT pointers
47+
// are only shared between the sender and server, to indicate persistence.
48+
repeated messaging.v1.Pointer pointers = 3 [(validate.rules).repeated = {
49+
min_items: 0
50+
max_items: 2
51+
}];
52+
}
53+
54+
message MetadataUpdate {
55+
oneof kind {
56+
option (validate.required) = true;
57+
58+
FullRefresh full_refresh = 1;
59+
LastActivityChanged last_activity_changed = 2;
60+
}
61+
62+
// Refreshes the entire chat metadata
63+
message FullRefresh {
64+
Metadata metadata = 1 [(validate.rules).message.required = true];
65+
}
66+
67+
// The last activity timestamp has changed to a newer value
68+
message LastActivityChanged {
69+
google.protobuf.Timestamp new_last_activity = 1 [(validate.rules).timestamp.required = true];
70+
}
71+
}

definitions/flipcash/protos/src/main/proto/common/v1/common.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ message UserId {
5858
}];
5959
}
6060

61+
message ChatId {
62+
bytes value = 1 [(validate.rules).bytes = {
63+
min_len: 32
64+
max_len: 32
65+
}];
66+
}
67+
6168
// AppInstallId is a unque ID tied to a client app installation. It does not
6269
// identify a device. Value should remain private and not be shared across
6370
// installs.

definitions/flipcash/protos/src/main/proto/contact/v1/contact_list_service.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import "validate/validate.proto";
99

1010
option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb";
1111
option java_package = "com.codeinc.flipcash.gen.contact.v1";
12+
option objc_class_prefix = "FPBContactV1";
1213

1314
// ContactList manages a user's contact list and surfaces which contacts are
1415
// Flipcash users.

definitions/flipcash/protos/src/main/proto/contact/v1/model.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "validate/validate.proto";
77

88
option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb";
99
option java_package = "com.codeinc.flipcash.gen.contact.v1";
10+
option objc_class_prefix = "FPBContactV1";
1011

1112
message FlipcashContact {
1213
phone.v1.PhoneNumber phone = 1 [(validate.rules).message.required = true];

definitions/flipcash/protos/src/main/proto/event/v1/model.proto

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/g
66
option java_package = "com.codeinc.flipcash.gen.events.v1";
77
option objc_class_prefix = "FPBEventV1";
88

9+
import "chat/v1/model.proto";
910
import "common/v1/common.proto";
11+
import "messaging/v1/model.proto";
1012
import "google/protobuf/duration.proto";
1113
import "google/protobuf/timestamp.proto";
1214
import "validate/validate.proto";
@@ -27,7 +29,8 @@ message Event {
2729
oneof type {
2830
option (validate.required) = true;
2931

30-
TestEvent test = 3;
32+
TestEvent test = 3;
33+
ChatUpdate chat_update = 4;
3134
}
3235
}
3336

@@ -71,3 +74,22 @@ message ClientPong {
7174
// of potential network latency
7275
google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true];
7376
}
77+
78+
message ChatUpdate {
79+
// The chat that this update is for
80+
common.v1.ChatId chat = 1 [(validate.rules).message.required = true];
81+
82+
// If present, new real-time messages sent on the chat
83+
messaging.v1.MessageBatch new_messages = 2;
84+
85+
// If present, message pointer updates for members in the chat
86+
messaging.v1.PointerBatch pointer_updates = 3;
87+
88+
// If present, message typing notification state changes for members in the chat
89+
messaging.v1.IsTypingNotificationBatch is_typing_notifications = 4;
90+
91+
// If present, updates to the chat metadata
92+
repeated chat.v1.MetadataUpdate metadata_updates = 5 [(validate.rules).repeated = {
93+
max_items: 1024 // Arbitrary
94+
}];
95+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
syntax = "proto3";
2+
3+
package flipcash.messaging.v1;
4+
5+
option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/messaging/v1;messagingpb";
6+
option java_package = "com.codeinc.flipcash.gen.messaging.v1";
7+
option objc_class_prefix = "FPBMessagingV1";
8+
9+
import "common/v1/common.proto";
10+
import "messaging/v1/model.proto";
11+
import "validate/validate.proto";
12+
13+
service Messaging {
14+
// GetMessage gets a single message in a chat
15+
rpc GetMessage(GetMessageRequest) returns (GetMessageResponse);
16+
17+
// GetMessages gets the set of messages for a chat using a paged and batched APIs
18+
rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse);
19+
20+
// SendMessage sends a message to a chat.
21+
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
22+
23+
// AdvancePointer advances a pointer in message history for a chat member.
24+
rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse);
25+
26+
// NotifyIsTypingRequest notifies a chat that the sending member is typing.
27+
//
28+
// These requests are transient, and may be dropped at any point.
29+
rpc NotifyIsTyping(NotifyIsTypingRequest) returns (NotifyIsTypingResponse);
30+
}
31+
32+
message GetMessageRequest {
33+
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];
34+
35+
MessageId message_id = 2 [(validate.rules).message.required = true];
36+
37+
common.v1.Auth auth = 10;
38+
}
39+
40+
message GetMessageResponse {
41+
Result result = 1;
42+
enum Result {
43+
OK = 0;
44+
DENIED = 1;
45+
NOT_FOUND = 2;
46+
}
47+
48+
Message message = 2;
49+
}
50+
51+
message GetMessagesRequest {
52+
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];
53+
54+
oneof query {
55+
option (validate.required) = true;
56+
57+
common.v1.QueryOptions options = 2;
58+
MessageIdBatch message_ids = 3;
59+
}
60+
61+
common.v1.Auth auth = 10;
62+
}
63+
64+
message GetMessagesResponse {
65+
Result result = 1;
66+
enum Result {
67+
OK = 0;
68+
DENIED = 1;
69+
NOT_FOUND = 2;
70+
}
71+
72+
MessageBatch messages = 2;
73+
}
74+
75+
message SendMessageRequest {
76+
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];
77+
78+
// Allowed content types that can be sent by client:
79+
// - TextContent
80+
repeated Content content = 2 [(validate.rules).repeated = {
81+
min_items: 1
82+
max_items: 1
83+
}];
84+
85+
// Client-generated idempotency token for this send. Used to dedup retried
86+
// sends and to correlate the optimistic local echo with the server-assigned
87+
// message returned in the response.
88+
ClientMessageId client_message_id = 3 [(validate.rules).message.required = true];
89+
90+
common.v1.Auth auth = 10 [(validate.rules).message.required = true];
91+
}
92+
93+
message SendMessageResponse {
94+
Result result = 1;
95+
enum Result {
96+
OK = 0;
97+
DENIED = 1;
98+
}
99+
100+
// The chat message that was sent if the RPC was succesful, which includes
101+
// server-side metadata like the generated message ID and official timestamp
102+
Message message = 2;
103+
}
104+
105+
message AdvancePointerRequest {
106+
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];
107+
108+
Pointer.Type pointer_type = 2 [(validate.rules).enum = {
109+
in: [2, 3] // DELIVERED, READ
110+
}];
111+
112+
MessageId new_value = 3 [(validate.rules).message.required = true];
113+
114+
common.v1.Auth auth = 10 [(validate.rules).message.required = true];
115+
}
116+
117+
message AdvancePointerResponse {
118+
Result result = 1;
119+
enum Result {
120+
OK = 0;
121+
DENIED = 1;
122+
MESSAGE_NOT_FOUND = 2;
123+
}
124+
}
125+
126+
message NotifyIsTypingRequest {
127+
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];
128+
129+
IsTypingNotification.State state = 2;
130+
131+
common.v1.Auth auth = 10 [(validate.rules).message.required = true];
132+
}
133+
134+
message NotifyIsTypingResponse {
135+
Result result = 1;
136+
enum Result {
137+
OK = 0;
138+
DENIED = 1;
139+
}
140+
}

0 commit comments

Comments
 (0)