Add chat client

This commit is contained in:
Ben Toogood
2020-10-15 15:05:17 +01:00
parent 68f3e52656
commit 9e1c0e990f
7 changed files with 209 additions and 277 deletions

121
chat/client/main.go Normal file
View File

@@ -0,0 +1,121 @@
// Package main is a client for the chat service to demonstrate how it would work for a client. To
// run the client, first launch the chat service by running `micro run ./chat` from the top level of
// this repo. Then run `micro run ./chat/client` and `micro logs -f client` to follow the logs of
// the client.
package main
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/context/metadata"
"github.com/micro/micro/v3/service/logger"
chat "github.com/micro/services/chat/proto"
)
var (
userOneID = "user-one-" + uuid.New().String()
userTwoID = "user-two-" + uuid.New().String()
)
func main() {
// create a chat service client
srv := service.New()
cli := chat.NewChatService("chat", srv.Client())
// create a chat for our users
userIDs := []string{userOneID, userTwoID}
nRsp, err := cli.New(context.TODO(), &chat.NewRequest{UserIds: userIDs})
if err != nil {
logger.Fatalf("Error creating the chat: %v", err)
}
chatID := nRsp.GetChatId()
logger.Infof("Chat Created. ID: %v", chatID)
// list the number messages in the chat history
hRsp, err := cli.History(context.TODO(), &chat.HistoryRequest{ChatId: chatID})
if err != nil {
logger.Fatalf("Error getting the chat history: %v", err)
}
logger.Infof("Chat has %v message(s)", len(hRsp.Messages))
// create a channel to handle errors
errChan := make(chan error)
// run user one
go func() {
ctx := metadata.NewContext(context.TODO(), metadata.Metadata{
"user-id": userOneID, "chat-id": chatID,
})
stream, err := cli.Connect(ctx)
if err != nil {
errChan <- err
return
}
for i := 1; true; i++ {
// send a message to the chat
err = stream.Send(&chat.Message{
ClientId: uuid.New().String(),
SentAt: time.Now().Unix(),
Subject: "Message from user one",
Text: fmt.Sprintf("Message #%v", i),
})
if err != nil {
errChan <- err
return
}
logger.Infof("User one sent message")
// wait for user two to respond
msg, err := stream.Recv()
if err != nil {
errChan <- err
return
}
logger.Infof("User one recieved message %v from %v", msg.Text, msg.UserId)
time.Sleep(time.Second)
}
}()
// run user two
go func() {
ctx := metadata.NewContext(context.TODO(), metadata.Metadata{
"user-id": userTwoID, "chat-id": chatID,
})
stream, err := cli.Connect(ctx)
if err != nil {
errChan <- err
return
}
for i := 1; true; i++ {
// send a response to the chat
err = stream.Send(&chat.Message{
ClientId: uuid.New().String(),
SentAt: time.Now().Unix(),
Subject: "Response from user two",
Text: fmt.Sprintf("Response #%v", i),
})
if err != nil {
errChan <- err
return
}
logger.Infof("User two sent message")
// wait for a message from user one
msg, err := stream.Recv()
if err != nil {
errChan <- err
return
}
logger.Infof("User two recieved message %v from %v", msg.Text, msg.UserId)
time.Sleep(time.Second)
}
}()
logger.Fatal(<-errChan)
}

View File

@@ -7,12 +7,13 @@ import (
"github.com/micro/micro/v3/service/context/metadata"
// import the proto, it's standard to import the services own proto under the alias pb
"github.com/google/uuid"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/events"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
// it's standard to import the services own proto under the alias pb
pb "github.com/micro/services/chat/proto"
)
@@ -22,8 +23,9 @@ func New() pb.ChatHandler {
}
const (
storeKeyPrefix = "chat/"
eventKeyPrefix = "chat/"
chatStoreKeyPrefix = "chats/"
chatEventKeyPrefix = "chats/"
messageStoreKeyPrefix = "messages/"
)
// handler satisfies the ChatHandler interface. You can see this inteface defined in chat.pb.micro.go
@@ -58,11 +60,11 @@ func (h *handler) New(ctx context.Context, req *pb.NewRequest, rsp *pb.NewRespon
sort.Strings(sortedIDs)
// key to lookup the chat in the store using, e.g. "chat/usera-userb-userc"
key := storeKeyPrefix + strings.Join(sortedIDs, "-")
key := chatStoreKeyPrefix + strings.Join(sortedIDs, "-")
// read from the store to check if a chat with these users already exists
recs, err := store.Read(key)
if err != nil {
if err == nil {
// if an error wasn't returned, at least one record was found. The value returned by the store
// is the bytes representation of the chat id. We'll convert this back into a string and return
// it to the client.
@@ -80,9 +82,9 @@ func (h *handler) New(ctx context.Context, req *pb.NewRequest, rsp *pb.NewRespon
// no chat id was returned so we'll generate one, write it to the store and then return it to the
// client
chatID := uuid.New().String()
record := store.Record{Key: key, Value: []byte(chatID)}
record := store.Record{Key: chatStoreKeyPrefix + chatID, Value: []byte(chatID)}
if err := store.Write(&record); err != nil {
logger.Errorf("Error writing to the store. Key: %v. Error: %v", key, err)
logger.Errorf("Error writing to the store. Key: %v. Error: %v", record.Key, err)
return errors.InternalServerError("chat.New.Unknown", "Error writing to the store")
}
@@ -104,7 +106,7 @@ func (h *handler) History(ctx context.Context, req *pb.HistoryRequest, rsp *pb.H
}
// lookup the chat from the store to ensure it's valid
if _, err := store.Read(storeKeyPrefix + req.ChatId); err == store.ErrNotFound {
if _, err := store.Read(chatStoreKeyPrefix + req.ChatId); err == store.ErrNotFound {
return errors.BadRequest("chat.History.InvalidChatID", "Chat not found with this ID")
} else if err != nil {
logger.Errorf("Error reading from the store. Chat ID: %v. Error: %v", req.ChatId, err)
@@ -114,7 +116,7 @@ func (h *handler) History(ctx context.Context, req *pb.HistoryRequest, rsp *pb.H
// lookup the historical messages for the chat using the event store. lots of packages in micro
// support options, in this case we'll pass the ReadLimit option to restrict the number of messages
// we'll load from the events store.
messages, err := events.Read(eventKeyPrefix+req.ChatId, events.ReadLimit(50))
messages, err := events.Read(chatEventKeyPrefix+req.ChatId, events.ReadLimit(50))
if err != nil {
logger.Errorf("Error reading from the event store. Chat ID: %v. Error: %v", req.ChatId, err)
return errors.InternalServerError("chat.History.Unknown", "Error reading from the event store")
@@ -143,17 +145,17 @@ func (h *handler) History(ctx context.Context, req *pb.HistoryRequest, rsp *pb.H
func (h *handler) Connect(ctx context.Context, stream pb.Chat_ConnectStream) error {
// the client passed the chat id and user id in the request context. we'll load that information
// now and validate it. If any information is missing we'll return a BadRequest error to the client
userID, ok := metadata.Get(ctx, "UserID")
userID, ok := metadata.Get(ctx, "user-id")
if !ok {
return errors.BadRequest("chat.Connect.MissingUserID", "UserID missing in context")
}
chatID, ok := metadata.Get(ctx, "ChatID")
chatID, ok := metadata.Get(ctx, "chat-id")
if !ok {
return errors.BadRequest("chat.Connect.MissingChatID", "ChatId missing in context")
}
// lookup the chat from the store to ensure it's valid
if _, err := store.Read(storeKeyPrefix + chatID); err == store.ErrNotFound {
if _, err := store.Read(chatStoreKeyPrefix + chatID); err == store.ErrNotFound {
return errors.BadRequest("chat.Connect.InvalidChatID", "Chat not found with this ID")
} else if err != nil {
logger.Errorf("Error reading from the store. Chat ID: %v. Error: %v", chatID, err)
@@ -175,9 +177,8 @@ func (h *handler) Connect(ctx context.Context, stream pb.Chat_ConnectStream) err
// create an event stream to consume messages posted by other users into the chat. we'll use the
// user id as a queue to ensure each user recieves the message
evStream, err := events.Subscribe(eventKeyPrefix+chatID, events.WithQueue(userID))
evStream, err := events.Subscribe(chatEventKeyPrefix+chatID, events.WithQueue(userID))
if err != nil {
defer cancel()
logger.Errorf("Error streaming events. Chat ID: %v. Error: %v", chatID, err)
return errors.InternalServerError("chat.Connect.Unknown", "Error connecting to the event stream")
}
@@ -235,8 +236,26 @@ func (h *handler) Connect(ctx context.Context, stream pb.Chat_ConnectStream) err
// an error occured in another goroutine, terminate the stream
return err
case msg := <-msgChan:
// a message was recieved from the client, send it to the event stream
if err := events.Publish(eventKeyPrefix+chatID, msg); err != nil {
// a message was recieved from the client. validate it hasn't been recieved before
if _, err := store.Read(messageStoreKeyPrefix + msg.ClientId); err == nil {
// the message has already been processed
continue
} else if err != store.ErrNotFound {
// an unexpected error occured
return err
}
// set the defaults
msg.UserId = userID
msg.ChatId = chatID
// send the message to the event stream
if err := events.Publish(chatEventKeyPrefix+chatID, msg); err != nil {
return err
}
// record the messages client id
if err := store.Write(&store.Record{Key: messageStoreKeyPrefix + msg.ClientId}); err != nil {
return err
}
}

View File

@@ -1,5 +1,5 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: proto/chat.proto
// source: chat/proto/chat.proto
package chat
@@ -32,7 +32,7 @@ func (m *NewRequest) Reset() { *m = NewRequest{} }
func (m *NewRequest) String() string { return proto.CompactTextString(m) }
func (*NewRequest) ProtoMessage() {}
func (*NewRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_ed7e7dde45555b7d, []int{0}
return fileDescriptor_825b1469f80f958d, []int{0}
}
func (m *NewRequest) XXX_Unmarshal(b []byte) error {
@@ -72,7 +72,7 @@ func (m *NewResponse) Reset() { *m = NewResponse{} }
func (m *NewResponse) String() string { return proto.CompactTextString(m) }
func (*NewResponse) ProtoMessage() {}
func (*NewResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_ed7e7dde45555b7d, []int{1}
return fileDescriptor_825b1469f80f958d, []int{1}
}
func (m *NewResponse) XXX_Unmarshal(b []byte) error {
@@ -114,7 +114,7 @@ func (m *HistoryRequest) Reset() { *m = HistoryRequest{} }
func (m *HistoryRequest) String() string { return proto.CompactTextString(m) }
func (*HistoryRequest) ProtoMessage() {}
func (*HistoryRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_ed7e7dde45555b7d, []int{2}
return fileDescriptor_825b1469f80f958d, []int{2}
}
func (m *HistoryRequest) XXX_Unmarshal(b []byte) error {
@@ -154,7 +154,7 @@ func (m *HistoryResponse) Reset() { *m = HistoryResponse{} }
func (m *HistoryResponse) String() string { return proto.CompactTextString(m) }
func (*HistoryResponse) ProtoMessage() {}
func (*HistoryResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_ed7e7dde45555b7d, []int{3}
return fileDescriptor_825b1469f80f958d, []int{3}
}
func (m *HistoryResponse) XXX_Unmarshal(b []byte) error {
@@ -193,7 +193,7 @@ type Message struct {
// id of the user who sent the message
UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
// time time the message was sent in unix format
SentAt int32 `protobuf:"varint,5,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
SentAt int64 `protobuf:"varint,5,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
// subject of the message
Subject string `protobuf:"bytes,6,opt,name=subject,proto3" json:"subject,omitempty"`
// text of the message
@@ -207,7 +207,7 @@ func (m *Message) Reset() { *m = Message{} }
func (m *Message) String() string { return proto.CompactTextString(m) }
func (*Message) ProtoMessage() {}
func (*Message) Descriptor() ([]byte, []int) {
return fileDescriptor_ed7e7dde45555b7d, []int{4}
return fileDescriptor_825b1469f80f958d, []int{4}
}
func (m *Message) XXX_Unmarshal(b []byte) error {
@@ -256,7 +256,7 @@ func (m *Message) GetUserId() string {
return ""
}
func (m *Message) GetSentAt() int32 {
func (m *Message) GetSentAt() int64 {
if m != nil {
return m.SentAt
}
@@ -285,30 +285,30 @@ func init() {
proto.RegisterType((*Message)(nil), "chat.Message")
}
func init() { proto.RegisterFile("proto/chat.proto", fileDescriptor_ed7e7dde45555b7d) }
func init() { proto.RegisterFile("chat/proto/chat.proto", fileDescriptor_825b1469f80f958d) }
var fileDescriptor_ed7e7dde45555b7d = []byte{
var fileDescriptor_825b1469f80f958d = []byte{
// 351 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0x41, 0x4f, 0xf2, 0x40,
0x10, 0xcd, 0x42, 0x69, 0x61, 0xc8, 0xc7, 0x87, 0x1b, 0x8d, 0x2b, 0x5e, 0x48, 0x0f, 0x5a, 0x24,
0xa1, 0x06, 0x13, 0x2f, 0x7a, 0x51, 0x2e, 0x72, 0x90, 0x43, 0x8f, 0x5e, 0x48, 0x69, 0x37, 0xb0,
0x46, 0xba, 0xd8, 0xd9, 0x8a, 0xfe, 0x12, 0x7f, 0x86, 0x7f, 0xd1, 0xec, 0x6e, 0x41, 0x30, 0xf1,
0x36, 0xef, 0xbd, 0x99, 0xd9, 0x37, 0x2f, 0x0b, 0xed, 0x55, 0x2e, 0x95, 0x0c, 0x93, 0x45, 0xac,
0x06, 0xa6, 0xa4, 0x8e, 0xae, 0xfd, 0x73, 0x80, 0x09, 0x5f, 0x47, 0xfc, 0xb5, 0xe0, 0xa8, 0xe8,
0x09, 0xd4, 0x0b, 0xe4, 0xf9, 0x54, 0xa4, 0xc8, 0x48, 0xb7, 0x1a, 0x34, 0x22, 0x4f, 0xe3, 0x71,
0x8a, 0xfe, 0x19, 0x34, 0x4d, 0x23, 0xae, 0x64, 0x86, 0x9c, 0x1e, 0x83, 0xa7, 0xe7, 0xa7, 0x22,
0x65, 0xa4, 0x4b, 0x82, 0x46, 0xe4, 0x6a, 0x38, 0x4e, 0xfd, 0x1e, 0xb4, 0x1e, 0x04, 0x2a, 0x99,
0x7f, 0x6c, 0x96, 0xfe, 0xd9, 0x7a, 0x0b, 0xff, 0xb7, 0xad, 0xe5, 0xda, 0x1e, 0xd4, 0x97, 0x1c,
0x31, 0x9e, 0x73, 0x6b, 0xa0, 0x39, 0xfc, 0x37, 0x30, 0x9e, 0x1f, 0x2d, 0x1b, 0x6d, 0x65, 0xff,
0x8b, 0x80, 0x57, 0xb2, 0xb4, 0x05, 0x95, 0xed, 0xf6, 0x8a, 0x48, 0xe9, 0x29, 0x34, 0x92, 0x17,
0xc1, 0x33, 0xf3, 0x68, 0xc5, 0xd0, 0x75, 0x4b, 0x8c, 0xd3, 0x5d, 0x3f, 0xd5, 0x5d, 0x3f, 0x5a,
0x28, 0xaf, 0x67, 0x8e, 0x15, 0xec, 0xf1, 0x5a, 0x40, 0xbd, 0x2c, 0x56, 0xac, 0xd6, 0x25, 0x41,
0x2d, 0x72, 0x35, 0xbc, 0x53, 0x94, 0x81, 0x87, 0xc5, 0xec, 0x99, 0x27, 0x8a, 0xb9, 0x66, 0x62,
0x03, 0x29, 0x05, 0x47, 0xf1, 0x77, 0xc5, 0x3c, 0x43, 0x9b, 0x7a, 0xf8, 0x49, 0xc0, 0x19, 0x2d,
0x62, 0x45, 0x2f, 0xa0, 0x3a, 0xe1, 0x6b, 0xda, 0xb6, 0xa7, 0xfd, 0xe4, 0xdf, 0x39, 0xd8, 0x61,
0xca, 0x44, 0xae, 0xc1, 0x2b, 0x43, 0xa2, 0x87, 0x56, 0xdd, 0x8f, 0xb7, 0x73, 0xf4, 0x8b, 0x2d,
0xe7, 0xfa, 0xe0, 0x8d, 0x64, 0x96, 0x69, 0x2f, 0xfb, 0x11, 0x76, 0xf6, 0x61, 0x40, 0x2e, 0xc9,
0x7d, 0xff, 0xa9, 0x37, 0x17, 0x6a, 0x51, 0xcc, 0x06, 0x89, 0x5c, 0x86, 0x4b, 0x91, 0xe4, 0x32,
0x44, 0x9e, 0xbf, 0x89, 0x84, 0xa3, 0xf9, 0x33, 0xa1, 0xf9, 0x33, 0x37, 0xba, 0x9c, 0xb9, 0xa6,
0xbe, 0xfa, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x71, 0x1b, 0x44, 0xc0, 0x53, 0x02, 0x00, 0x00,
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0xcd, 0x4e, 0xf2, 0x40,
0x14, 0xcd, 0xd0, 0x7e, 0x1d, 0xb8, 0xe4, 0x43, 0x9d, 0x48, 0x1c, 0x71, 0x43, 0xba, 0xd0, 0x22,
0x09, 0x35, 0x98, 0xb8, 0xd1, 0x8d, 0xb2, 0x91, 0x85, 0x2c, 0xba, 0x74, 0x43, 0x4a, 0x3b, 0x81,
0x31, 0xd2, 0xc1, 0xce, 0x54, 0xf4, 0x49, 0x7c, 0x0c, 0x5f, 0xd1, 0xcc, 0x4c, 0xf9, 0xa9, 0x89,
0xbb, 0xf3, 0x33, 0xf7, 0xf6, 0xdc, 0x93, 0x42, 0x3b, 0x59, 0xc4, 0x2a, 0x5c, 0xe5, 0x42, 0x89,
0x50, 0xc3, 0x81, 0x81, 0xc4, 0xd5, 0xd8, 0xbf, 0x00, 0x98, 0xb0, 0x75, 0xc4, 0xde, 0x0a, 0x26,
0x15, 0x39, 0x85, 0x7a, 0x21, 0x59, 0x3e, 0xe5, 0xa9, 0xa4, 0xa8, 0xeb, 0x04, 0x8d, 0x08, 0x6b,
0x3e, 0x4e, 0xa5, 0x7f, 0x0e, 0x4d, 0xf3, 0x50, 0xae, 0x44, 0x26, 0x19, 0x39, 0x01, 0xac, 0xe7,
0xa7, 0x3c, 0xa5, 0xa8, 0x8b, 0x82, 0x46, 0xe4, 0x69, 0x3a, 0x4e, 0xfd, 0x1e, 0xb4, 0x1e, 0xb9,
0x54, 0x22, 0xff, 0xdc, 0x2c, 0xfd, 0xf3, 0xe9, 0x1d, 0x1c, 0x6c, 0x9f, 0x96, 0x6b, 0x7b, 0x50,
0x5f, 0x32, 0x29, 0xe3, 0x39, 0xb3, 0x01, 0x9a, 0xc3, 0xff, 0x03, 0x93, 0xf9, 0xc9, 0xaa, 0xd1,
0xd6, 0xf6, 0xbf, 0x11, 0xe0, 0x52, 0x25, 0x2d, 0xa8, 0x6d, 0xb7, 0xd7, 0x78, 0x4a, 0xce, 0xa0,
0x91, 0xbc, 0x72, 0x96, 0x99, 0x8f, 0xd6, 0x8c, 0x5c, 0xb7, 0xc2, 0x38, 0xdd, 0xcf, 0xe3, 0xec,
0xe7, 0xd1, 0x46, 0x79, 0x3d, 0x75, 0xad, 0x61, 0x8f, 0xd7, 0x86, 0xd4, 0xcb, 0x62, 0x45, 0xff,
0x75, 0x51, 0xe0, 0x44, 0x9e, 0xa6, 0xf7, 0x8a, 0x50, 0xc0, 0xb2, 0x98, 0xbd, 0xb0, 0x44, 0x51,
0xcf, 0x4c, 0x6c, 0x28, 0x21, 0xe0, 0x2a, 0xf6, 0xa1, 0x28, 0x36, 0xb2, 0xc1, 0xc3, 0x2f, 0x04,
0xee, 0x68, 0x11, 0x2b, 0x72, 0x09, 0xce, 0x84, 0xad, 0xc9, 0xa1, 0x3d, 0x6d, 0xd7, 0x7f, 0xe7,
0x68, 0x4f, 0x29, 0x1b, 0xb9, 0x01, 0x5c, 0x96, 0x44, 0x8e, 0xad, 0x5b, 0xad, 0xb7, 0xd3, 0xfe,
0xa5, 0x96, 0x73, 0x7d, 0xc0, 0x23, 0x91, 0x65, 0x3a, 0x4b, 0xb5, 0xc2, 0x4e, 0x95, 0x06, 0xe8,
0x0a, 0x3d, 0xf4, 0x9f, 0x7b, 0x73, 0xae, 0x16, 0xc5, 0x6c, 0x90, 0x88, 0x65, 0xb8, 0xe4, 0x49,
0x2e, 0x42, 0xc9, 0xf2, 0x77, 0x9e, 0x30, 0x19, 0xee, 0x7e, 0x9f, 0x5b, 0x0d, 0x67, 0x9e, 0xc1,
0xd7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x53, 0xab, 0xf2, 0x77, 0x58, 0x02, 0x00, 0x00,
}

View File

@@ -1,5 +1,5 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/chat.proto
// source: chat/proto/chat.proto
package chat

View File

@@ -50,7 +50,7 @@ message Message {
// id of the user who sent the message
string user_id = 4;
// time time the message was sent in unix format
int32 sent_at = 5;
int64 sent_at = 5;
// subject of the message
string subject = 6;
// text of the message