From b02d4dacb57ee6817d7fe3e0a23b120b40c913ae Mon Sep 17 00:00:00 2001 From: ben-toogood Date: Thu, 4 Feb 2021 11:05:32 +0000 Subject: [PATCH] Seen Service (#39) * Seen Service * Fixes for model * Update import * More fixes * Complete seen service --- go.sum | 2 +- seen/.gitignore | 2 + seen/Dockerfile | 3 + seen/Makefile | 22 ++ seen/domain/domain.go | 93 ++++++ seen/handler/handler.go | 119 +++++++ seen/handler/handler_test.go | 226 +++++++++++++ seen/main.go | 29 ++ seen/micro.mu | 1 + seen/proto/seen.pb.go | 614 +++++++++++++++++++++++++++++++++++ seen/proto/seen.pb.micro.go | 140 ++++++++ seen/proto/seen.proto | 52 +++ 12 files changed, 1302 insertions(+), 1 deletion(-) create mode 100644 seen/.gitignore create mode 100644 seen/Dockerfile create mode 100644 seen/Makefile create mode 100644 seen/domain/domain.go create mode 100644 seen/handler/handler.go create mode 100644 seen/handler/handler_test.go create mode 100644 seen/main.go create mode 100644 seen/micro.mu create mode 100644 seen/proto/seen.pb.go create mode 100644 seen/proto/seen.pb.micro.go create mode 100644 seen/proto/seen.proto diff --git a/go.sum b/go.sum index a1f46a8..ed825ee 100644 --- a/go.sum +++ b/go.sum @@ -153,7 +153,7 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsouza/go-dockerclient v1.4.4/go.mod h1:PrwszSL5fbmsESocROrOGq/NULMXRw+bajY0ltzD6MA= github.com/fsouza/go-dockerclient v1.6.0/go.mod h1:YWwtNPuL4XTX1SKJQk86cWPmmqwx+4np9qfPbb+znGc= -github.com/getkin/kin-openapi v0.26.0 h1:xKIW5Z5wAfutxGBH+rr9qu0Ywfb/E1bPWkYLKRYfEuU= +github.com/getkin/kin-openapi v0.26.0 h1:xKIW5Z5wAfutxGBH+rr9qu0Ywfb/E1bPWkYLKRYfEuU github.com/getkin/kin-openapi v0.26.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/seen/.gitignore b/seen/.gitignore new file mode 100644 index 0000000..49b4681 --- /dev/null +++ b/seen/.gitignore @@ -0,0 +1,2 @@ + +seen diff --git a/seen/Dockerfile b/seen/Dockerfile new file mode 100644 index 0000000..abf9e36 --- /dev/null +++ b/seen/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine +ADD seen /seen +ENTRYPOINT [ "/seen" ] diff --git a/seen/Makefile b/seen/Makefile new file mode 100644 index 0000000..19a15ed --- /dev/null +++ b/seen/Makefile @@ -0,0 +1,22 @@ + +GOPATH:=$(shell go env GOPATH) +.PHONY: init +init: + go get -u github.com/golang/protobuf/proto + go get -u github.com/golang/protobuf/protoc-gen-go + go get github.com/micro/micro/v3/cmd/protoc-gen-micro +.PHONY: proto +proto: + protoc --proto_path=. --micro_out=. --go_out=:. proto/seen.proto + +.PHONY: build +build: + go build -o seen *.go + +.PHONY: test +test: + go test -v ./... -cover + +.PHONY: docker +docker: + docker build . -t seen:latest diff --git a/seen/domain/domain.go b/seen/domain/domain.go new file mode 100644 index 0000000..1e8c751 --- /dev/null +++ b/seen/domain/domain.go @@ -0,0 +1,93 @@ +package domain + +import ( + "time" + + "github.com/micro/micro/v3/service/model" + "github.com/micro/micro/v3/service/store" +) + +// Seen is the object which represents a user seeing a resource +type Seen struct { + ID string + UserID string + ResourceID string + ResourceType string + Timestamp time.Time +} + +type Domain struct { + db model.Model +} + +var ( + userIDIndex = model.ByEquality("UserID") + resourceIDIndex = model.ByEquality("ResourceID") + resourceTypeIndex = model.ByEquality("ResourceType") +) + +func New(store store.Store) *Domain { + db := model.New(store, Seen{}, []model.Index{ + userIDIndex, resourceIDIndex, resourceTypeIndex, + }, &model.ModelOptions{}) + + return &Domain{db: db} +} + +// Create a seen object in the store +func (d *Domain) Create(s Seen) error { + return d.db.Create(s) +} + +// Delete a seen object from the store +func (d *Domain) Delete(s Seen) error { + // load all the users objects and then delete only the ones which match the resource, unfortunately + // the model doesn't yet support querying by multiple columns + var all []Seen + if err := d.db.Read(model.Equals("UserID", s.UserID), &all); err != nil { + return err + } + for _, a := range all { + if s.ResourceID != a.ResourceID { + continue + } + if s.ResourceType != s.ResourceType { + continue + } + + q := model.Equals("ID", a.ID) + q.Order.Type = model.OrderTypeUnordered + if err := d.db.Delete(q); err != nil { + return err + } + } + return nil +} + +// Read the timestamps from the store +func (d *Domain) Read(userID, resourceType string, resourceIDs []string) (map[string]time.Time, error) { + // load all the users objects and then return only the timestamps for the ones which match the + // resource, unfortunately the model doesn't yet support querying by multiple columns + var all []Seen + if err := d.db.Read(model.Equals("UserID", userID), &all); err != nil { + return nil, err + } + + result := map[string]time.Time{} + for _, a := range all { + if a.ResourceType != resourceType { + continue + } + + for _, id := range resourceIDs { + if id != a.ResourceID { + continue + } + + result[id] = a.Timestamp + break + } + } + + return result, nil +} diff --git a/seen/handler/handler.go b/seen/handler/handler.go new file mode 100644 index 0000000..d41cda0 --- /dev/null +++ b/seen/handler/handler.go @@ -0,0 +1,119 @@ +package handler + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/micro/services/seen/domain" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/seen/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + ErrMissingUserID = errors.BadRequest("MISSING_USER_ID", "Missing UserID") + ErrMissingResourceID = errors.BadRequest("MISSING_RESOURCE_ID", "Missing ResourceID") + ErrMissingResourceIDs = errors.BadRequest("MISSING_RESOURCE_IDS", "Missing ResourceIDs") + ErrMissingResourceType = errors.BadRequest("MISSING_RESOURCE_TYPE", "Missing ResourceType") + ErrStore = errors.InternalServerError("STORE_ERROR", "Error connecting to the store") +) + +type Seen struct { + Domain *domain.Domain +} + +// Set a resource as seen by a user. If no timestamp is provided, the current time is used. +func (s *Seen) Set(ctx context.Context, req *pb.SetRequest, rsp *pb.SetResponse) error { + // validate the request + if len(req.UserId) == 0 { + return ErrMissingUserID + } + if len(req.ResourceId) == 0 { + return ErrMissingResourceID + } + if len(req.ResourceType) == 0 { + return ErrMissingResourceType + } + + // default the timestamp + if req.Timestamp == nil { + req.Timestamp = timestamppb.New(time.Now()) + } + + // write the object to the store + err := s.Domain.Create(domain.Seen{ + ID: uuid.New().String(), + UserID: req.UserId, + ResourceID: req.ResourceId, + ResourceType: req.ResourceType, + Timestamp: req.Timestamp.AsTime(), + }) + if err != nil { + logger.Errorf("Error with store: %v", err) + return ErrStore + } + + return nil +} + +// Unset a resource as seen, used in cases where a user viewed a resource but wants to override +// this so they remember to action it in the future, e.g. "Mark this as unread". +func (s *Seen) Unset(ctx context.Context, req *pb.UnsetRequest, rsp *pb.UnsetResponse) error { + // validate the request + if len(req.UserId) == 0 { + return ErrMissingUserID + } + if len(req.ResourceId) == 0 { + return ErrMissingResourceID + } + if len(req.ResourceType) == 0 { + return ErrMissingResourceType + } + + // delete the object from the store + err := s.Domain.Delete(domain.Seen{ + UserID: req.UserId, + ResourceID: req.ResourceId, + ResourceType: req.ResourceType, + }) + if err != nil { + logger.Errorf("Error with store: %v", err) + return ErrStore + } + + return nil +} + +// Read returns the timestamps at which various resources were seen by a user. If no timestamp +// is returned for a given resource_id, it indicates that resource has not yet been seen by the +// user. +func (s *Seen) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error { + // validate the request + if len(req.UserId) == 0 { + return ErrMissingUserID + } + if len(req.ResourceIds) == 0 { + return ErrMissingResourceIDs + } + if len(req.ResourceType) == 0 { + return ErrMissingResourceType + } + + // query the store + data, err := s.Domain.Read(req.UserId, req.ResourceType, req.ResourceIds) + if err != nil { + logger.Errorf("Error with store: %v", err) + return ErrStore + } + + // serialize the response + rsp.Timestamps = make(map[string]*timestamppb.Timestamp, len(data)) + for uid, ts := range data { + rsp.Timestamps[uid] = timestamppb.New(ts) + } + + return nil +} diff --git a/seen/handler/handler_test.go b/seen/handler/handler_test.go new file mode 100644 index 0000000..1b48a3a --- /dev/null +++ b/seen/handler/handler_test.go @@ -0,0 +1,226 @@ +package handler_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/google/uuid" + "github.com/micro/micro/v3/service/store/memory" + "github.com/micro/services/seen/domain" + "github.com/micro/services/seen/handler" + pb "github.com/micro/services/seen/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func newHandler() *handler.Seen { + return &handler.Seen{ + Domain: domain.New(memory.NewStore()), + } +} + +func TestSet(t *testing.T) { + tt := []struct { + Name string + UserID string + ResourceType string + ResourceID string + Timestamp *timestamppb.Timestamp + Error error + }{ + { + Name: "MissingUserID", + ResourceType: "message", + ResourceID: uuid.New().String(), + Error: handler.ErrMissingUserID, + }, + { + Name: "MissingResourceID", + UserID: uuid.New().String(), + ResourceType: "message", + Error: handler.ErrMissingResourceID, + }, + { + Name: "MissingResourceType", + UserID: uuid.New().String(), + ResourceID: uuid.New().String(), + Error: handler.ErrMissingResourceType, + }, + { + Name: "WithTimetamp", + UserID: uuid.New().String(), + ResourceID: uuid.New().String(), + ResourceType: "message", + Timestamp: timestamppb.New(time.Now().Add(time.Minute * -5)), + }, + { + Name: "WithoutTimetamp", + UserID: uuid.New().String(), + ResourceID: uuid.New().String(), + ResourceType: "message", + }, + } + + h := newHandler() + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + err := h.Set(context.TODO(), &pb.SetRequest{ + UserId: tc.UserID, + ResourceId: tc.ResourceID, + ResourceType: tc.ResourceType, + Timestamp: tc.Timestamp, + }, &pb.SetResponse{}) + + assert.Equal(t, tc.Error, err) + }) + } +} +func TestUnset(t *testing.T) { + // seed some test data + h := newHandler() + seed := &pb.SetRequest{ + UserId: uuid.New().String(), + ResourceId: uuid.New().String(), + ResourceType: "message", + } + err := h.Set(context.TODO(), seed, &pb.SetResponse{}) + assert.NoError(t, err) + + tt := []struct { + Name string + UserID string + ResourceType string + ResourceID string + Error error + }{ + { + Name: "MissingUserID", + ResourceType: "message", + ResourceID: uuid.New().String(), + Error: handler.ErrMissingUserID, + }, + { + Name: "MissingResourceID", + UserID: uuid.New().String(), + ResourceType: "message", + Error: handler.ErrMissingResourceID, + }, + { + Name: "MissingResourceType", + UserID: uuid.New().String(), + ResourceID: uuid.New().String(), + Error: handler.ErrMissingResourceType, + }, + { + Name: "Exists", + UserID: seed.UserId, + ResourceID: seed.ResourceId, + ResourceType: seed.ResourceType, + }, + { + Name: "Repeat", + UserID: seed.UserId, + ResourceID: seed.ResourceId, + ResourceType: seed.ResourceType, + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + err := h.Unset(context.TODO(), &pb.UnsetRequest{ + UserId: tc.UserID, + ResourceId: tc.ResourceID, + ResourceType: tc.ResourceType, + }, &pb.UnsetResponse{}) + + assert.Equal(t, tc.Error, err) + }) + } +} + +func TestRead(t *testing.T) { + tn := time.Now() + h := newHandler() + + // seed some test data + td := []struct { + UserID string + ResourceID string + ResourceType string + Timestamp *timestamppb.Timestamp + }{ + { + UserID: "user-1", + ResourceID: "message-1", + ResourceType: "message", + Timestamp: timestamppb.New(tn), + }, + { + UserID: "user-1", + ResourceID: "message-2", + ResourceType: "message", + Timestamp: timestamppb.New(tn.Add(time.Minute * -10)), + }, + { + UserID: "user-1", + ResourceID: "notification-1", + ResourceType: "notification", + Timestamp: timestamppb.New(tn.Add(time.Minute * -10)), + }, + { + UserID: "user-2", + ResourceID: "message-3", + ResourceType: "message", + Timestamp: timestamppb.New(tn.Add(time.Minute * -10)), + }, + } + for _, d := range td { + assert.NoError(t, h.Set(context.TODO(), &pb.SetRequest{ + UserId: d.UserID, + ResourceId: d.ResourceID, + ResourceType: d.ResourceType, + Timestamp: d.Timestamp, + }, &pb.SetResponse{})) + } + + // check only the requested values are returned + var rsp pb.ReadResponse + err := h.Read(context.TODO(), &pb.ReadRequest{ + UserId: "user-1", + ResourceType: "message", + ResourceIds: []string{"message-1", "message-2", "message-3"}, + }, &rsp) + assert.NoError(t, err) + assert.Len(t, rsp.Timestamps, 2) + + if v := rsp.Timestamps["message-1"]; v != nil { + assert.True(t, v.AsTime().Equal(tn)) + } else { + t.Errorf("Expected a timestamp for message-1") + } + + if v := rsp.Timestamps["message-2"]; v != nil { + assert.True(t, v.AsTime().Equal(tn.Add(time.Minute*-10))) + } else { + t.Errorf("Expected a timestamp for message-2") + } + + // unsetting a resource should remove it from the list + err = h.Unset(context.TODO(), &pb.UnsetRequest{ + UserId: "user-1", + ResourceId: "message-2", + ResourceType: "message", + }, &pb.UnsetResponse{}) + assert.NoError(t, err) + + rsp = pb.ReadResponse{} + err = h.Read(context.TODO(), &pb.ReadRequest{ + UserId: "user-1", + ResourceType: "message", + ResourceIds: []string{"message-1", "message-2", "message-3"}, + }, &rsp) + assert.NoError(t, err) + assert.Len(t, rsp.Timestamps, 1) +} diff --git a/seen/main.go b/seen/main.go new file mode 100644 index 0000000..cb7dbd7 --- /dev/null +++ b/seen/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/micro/services/seen/domain" + "github.com/micro/services/seen/handler" + pb "github.com/micro/services/seen/proto" + + "github.com/micro/micro/v3/service" + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" +) + +func main() { + // Create service + srv := service.New( + service.Name("seen"), + service.Version("latest"), + ) + + // Register handler + pb.RegisterSeenHandler(srv.Server(), &handler.Seen{ + Domain: domain.New(store.DefaultStore), + }) + + // Run service + if err := srv.Run(); err != nil { + logger.Fatal(err) + } +} diff --git a/seen/micro.mu b/seen/micro.mu new file mode 100644 index 0000000..74056f4 --- /dev/null +++ b/seen/micro.mu @@ -0,0 +1 @@ +service seen diff --git a/seen/proto/seen.pb.go b/seen/proto/seen.pb.go new file mode 100644 index 0000000..5dc925b --- /dev/null +++ b/seen/proto/seen.pb.go @@ -0,0 +1,614 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.23.0 +// protoc v3.13.0 +// source: proto/seen.proto + +package seen + +import ( + proto "github.com/golang/protobuf/proto" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type Resource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *Resource) Reset() { + *x = Resource{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Resource) ProtoMessage() {} + +func (x *Resource) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Resource.ProtoReflect.Descriptor instead. +func (*Resource) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{0} +} + +func (x *Resource) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Resource) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type SetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ResourceType string `protobuf:"bytes,2,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + ResourceId string `protobuf:"bytes,3,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + Timestamp *timestamp.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *SetRequest) Reset() { + *x = SetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetRequest) ProtoMessage() {} + +func (x *SetRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetRequest.ProtoReflect.Descriptor instead. +func (*SetRequest) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{1} +} + +func (x *SetRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *SetRequest) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *SetRequest) GetResourceId() string { + if x != nil { + return x.ResourceId + } + return "" +} + +func (x *SetRequest) GetTimestamp() *timestamp.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +type SetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SetResponse) Reset() { + *x = SetResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetResponse) ProtoMessage() {} + +func (x *SetResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetResponse.ProtoReflect.Descriptor instead. +func (*SetResponse) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{2} +} + +type UnsetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ResourceType string `protobuf:"bytes,2,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + ResourceId string `protobuf:"bytes,3,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` +} + +func (x *UnsetRequest) Reset() { + *x = UnsetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UnsetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetRequest) ProtoMessage() {} + +func (x *UnsetRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnsetRequest.ProtoReflect.Descriptor instead. +func (*UnsetRequest) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{3} +} + +func (x *UnsetRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *UnsetRequest) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *UnsetRequest) GetResourceId() string { + if x != nil { + return x.ResourceId + } + return "" +} + +type UnsetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UnsetResponse) Reset() { + *x = UnsetResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UnsetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetResponse) ProtoMessage() {} + +func (x *UnsetResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnsetResponse.ProtoReflect.Descriptor instead. +func (*UnsetResponse) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{4} +} + +type ReadRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ResourceType string `protobuf:"bytes,2,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + ResourceIds []string `protobuf:"bytes,3,rep,name=resource_ids,json=resourceIds,proto3" json:"resource_ids,omitempty"` +} + +func (x *ReadRequest) Reset() { + *x = ReadRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadRequest) ProtoMessage() {} + +func (x *ReadRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. +func (*ReadRequest) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{5} +} + +func (x *ReadRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ReadRequest) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *ReadRequest) GetResourceIds() []string { + if x != nil { + return x.ResourceIds + } + return nil +} + +type ReadResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamps map[string]*timestamp.Timestamp `protobuf:"bytes,1,rep,name=timestamps,proto3" json:"timestamps,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ReadResponse) Reset() { + *x = ReadResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_seen_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadResponse) ProtoMessage() {} + +func (x *ReadResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_seen_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. +func (*ReadResponse) Descriptor() ([]byte, []int) { + return file_proto_seen_proto_rawDescGZIP(), []int{6} +} + +func (x *ReadResponse) GetTimestamps() map[string]*timestamp.Timestamp { + if x != nil { + return x.Timestamps + } + return nil +} + +var File_proto_seen_proto protoreflect.FileDescriptor + +var file_proto_seen_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x04, 0x73, 0x65, 0x65, 0x6e, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2e, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xa5, 0x01, 0x0a, 0x0a, 0x53, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x6d, 0x0a, 0x0c, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x22, + 0x0f, 0x0a, 0x0d, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x6e, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x73, + 0x22, 0xad, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x42, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x73, 0x1a, 0x59, 0x0a, 0x0f, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x32, 0x93, 0x01, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x2a, 0x0a, 0x03, 0x53, 0x65, 0x74, + 0x12, 0x10, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x12, 0x12, + 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, + 0x11, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0c, 0x5a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, + 0x73, 0x65, 0x65, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_seen_proto_rawDescOnce sync.Once + file_proto_seen_proto_rawDescData = file_proto_seen_proto_rawDesc +) + +func file_proto_seen_proto_rawDescGZIP() []byte { + file_proto_seen_proto_rawDescOnce.Do(func() { + file_proto_seen_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_seen_proto_rawDescData) + }) + return file_proto_seen_proto_rawDescData +} + +var file_proto_seen_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_proto_seen_proto_goTypes = []interface{}{ + (*Resource)(nil), // 0: seen.Resource + (*SetRequest)(nil), // 1: seen.SetRequest + (*SetResponse)(nil), // 2: seen.SetResponse + (*UnsetRequest)(nil), // 3: seen.UnsetRequest + (*UnsetResponse)(nil), // 4: seen.UnsetResponse + (*ReadRequest)(nil), // 5: seen.ReadRequest + (*ReadResponse)(nil), // 6: seen.ReadResponse + nil, // 7: seen.ReadResponse.TimestampsEntry + (*timestamp.Timestamp)(nil), // 8: google.protobuf.Timestamp +} +var file_proto_seen_proto_depIdxs = []int32{ + 8, // 0: seen.SetRequest.timestamp:type_name -> google.protobuf.Timestamp + 7, // 1: seen.ReadResponse.timestamps:type_name -> seen.ReadResponse.TimestampsEntry + 8, // 2: seen.ReadResponse.TimestampsEntry.value:type_name -> google.protobuf.Timestamp + 1, // 3: seen.Seen.Set:input_type -> seen.SetRequest + 3, // 4: seen.Seen.Unset:input_type -> seen.UnsetRequest + 5, // 5: seen.Seen.Read:input_type -> seen.ReadRequest + 2, // 6: seen.Seen.Set:output_type -> seen.SetResponse + 4, // 7: seen.Seen.Unset:output_type -> seen.UnsetResponse + 6, // 8: seen.Seen.Read:output_type -> seen.ReadResponse + 6, // [6:9] is the sub-list for method output_type + 3, // [3:6] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_proto_seen_proto_init() } +func file_proto_seen_proto_init() { + if File_proto_seen_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_seen_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Resource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_seen_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_seen_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_seen_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UnsetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_seen_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UnsetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_seen_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReadRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_seen_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReadResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_seen_proto_rawDesc, + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_seen_proto_goTypes, + DependencyIndexes: file_proto_seen_proto_depIdxs, + MessageInfos: file_proto_seen_proto_msgTypes, + }.Build() + File_proto_seen_proto = out.File + file_proto_seen_proto_rawDesc = nil + file_proto_seen_proto_goTypes = nil + file_proto_seen_proto_depIdxs = nil +} diff --git a/seen/proto/seen.pb.micro.go b/seen/proto/seen.pb.micro.go new file mode 100644 index 0000000..47aba7d --- /dev/null +++ b/seen/proto/seen.pb.micro.go @@ -0,0 +1,140 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: proto/seen.proto + +package seen + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + _ "github.com/golang/protobuf/ptypes/timestamp" + math "math" +) + +import ( + context "context" + api "github.com/micro/micro/v3/service/api" + client "github.com/micro/micro/v3/service/client" + server "github.com/micro/micro/v3/service/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// Reference imports to suppress errors if they are not otherwise used. +var _ api.Endpoint +var _ context.Context +var _ client.Option +var _ server.Option + +// Api Endpoints for Seen service + +func NewSeenEndpoints() []*api.Endpoint { + return []*api.Endpoint{} +} + +// Client API for Seen service + +type SeenService interface { + // Set a resource as seen by a user. If no timestamp is provided, the current time is used. + Set(ctx context.Context, in *SetRequest, opts ...client.CallOption) (*SetResponse, error) + // Unset a resource as seen, used in cases where a user viewed a resource but wants to override + // this so they remember to action it in the future, e.g. "Mark this as unread". + Unset(ctx context.Context, in *UnsetRequest, opts ...client.CallOption) (*UnsetResponse, error) + // Read returns the timestamps at which various resources were seen by a user. If no timestamp + // is returned for a given resource_id, it indicates that resource has not yet been seen by the + // user. + Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) +} + +type seenService struct { + c client.Client + name string +} + +func NewSeenService(name string, c client.Client) SeenService { + return &seenService{ + c: c, + name: name, + } +} + +func (c *seenService) Set(ctx context.Context, in *SetRequest, opts ...client.CallOption) (*SetResponse, error) { + req := c.c.NewRequest(c.name, "Seen.Set", in) + out := new(SetResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *seenService) Unset(ctx context.Context, in *UnsetRequest, opts ...client.CallOption) (*UnsetResponse, error) { + req := c.c.NewRequest(c.name, "Seen.Unset", in) + out := new(UnsetResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *seenService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) { + req := c.c.NewRequest(c.name, "Seen.Read", in) + out := new(ReadResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Seen service + +type SeenHandler interface { + // Set a resource as seen by a user. If no timestamp is provided, the current time is used. + Set(context.Context, *SetRequest, *SetResponse) error + // Unset a resource as seen, used in cases where a user viewed a resource but wants to override + // this so they remember to action it in the future, e.g. "Mark this as unread". + Unset(context.Context, *UnsetRequest, *UnsetResponse) error + // Read returns the timestamps at which various resources were seen by a user. If no timestamp + // is returned for a given resource_id, it indicates that resource has not yet been seen by the + // user. + Read(context.Context, *ReadRequest, *ReadResponse) error +} + +func RegisterSeenHandler(s server.Server, hdlr SeenHandler, opts ...server.HandlerOption) error { + type seen interface { + Set(ctx context.Context, in *SetRequest, out *SetResponse) error + Unset(ctx context.Context, in *UnsetRequest, out *UnsetResponse) error + Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error + } + type Seen struct { + seen + } + h := &seenHandler{hdlr} + return s.Handle(s.NewHandler(&Seen{h}, opts...)) +} + +type seenHandler struct { + SeenHandler +} + +func (h *seenHandler) Set(ctx context.Context, in *SetRequest, out *SetResponse) error { + return h.SeenHandler.Set(ctx, in, out) +} + +func (h *seenHandler) Unset(ctx context.Context, in *UnsetRequest, out *UnsetResponse) error { + return h.SeenHandler.Unset(ctx, in, out) +} + +func (h *seenHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error { + return h.SeenHandler.Read(ctx, in, out) +} diff --git a/seen/proto/seen.proto b/seen/proto/seen.proto new file mode 100644 index 0000000..7570f7e --- /dev/null +++ b/seen/proto/seen.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package seen; +option go_package = "proto;seen"; +import "google/protobuf/timestamp.proto"; + +// Seen is a service to keep track of which resources a user has seen (read). For example, it can +// be used to keep track of what notifications have been seen by a user, or what messages they've +// read in a chat. +service Seen { + // Set a resource as seen by a user. If no timestamp is provided, the current time is used. + rpc Set(SetRequest) returns (SetResponse); + // Unset a resource as seen, used in cases where a user viewed a resource but wants to override + // this so they remember to action it in the future, e.g. "Mark this as unread". + rpc Unset(UnsetRequest) returns (UnsetResponse); + // Read returns the timestamps at which various resources were seen by a user. If no timestamp + // is returned for a given resource_id, it indicates that resource has not yet been seen by the + // user. + rpc Read(ReadRequest) returns (ReadResponse); +} + +message Resource { + string type = 1; + string id = 2; +} + +message SetRequest { + string user_id = 1; + string resource_type = 2; + string resource_id = 3; + google.protobuf.Timestamp timestamp = 4; +} + +message SetResponse {} + +message UnsetRequest { + string user_id = 1; + string resource_type = 2; + string resource_id = 3; +} + +message UnsetResponse {} + +message ReadRequest { + string user_id = 1; + string resource_type = 2; + repeated string resource_ids = 3; +} + +message ReadResponse { + map timestamps = 1; +} \ No newline at end of file