diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-02-10 13:52:05 -0500 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2024-02-10 13:52:05 -0500 |
| commit | 4c5c287968863af8442f95931ef88de2f9f60e7a (patch) | |
| tree | 120e011e646adb68394c1a50e30f0ce5a7742e50 /cmd/mimi/modules/scheduling | |
| parent | aabf9894594232754f0a0a8ce364a07c9aad5f2a (diff) | |
| download | x-4c5c287968863af8442f95931ef88de2f9f60e7a.tar.xz x-4c5c287968863af8442f95931ef88de2f9f60e7a.zip | |
cmd/mimi: introduce grpc
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd/mimi/modules/scheduling')
| -rw-r--r-- | cmd/mimi/modules/scheduling/scheduling.go | 77 | ||||
| -rw-r--r-- | cmd/mimi/modules/scheduling/scheduling.pb.go | 377 | ||||
| -rw-r--r-- | cmd/mimi/modules/scheduling/scheduling.twirp.go | 1113 | ||||
| -rw-r--r-- | cmd/mimi/modules/scheduling/scheduling_grpc.pb.go | 109 | ||||
| -rw-r--r-- | cmd/mimi/modules/scheduling/scheduling_validate.go | 55 |
5 files changed, 1672 insertions, 59 deletions
diff --git a/cmd/mimi/modules/scheduling/scheduling.go b/cmd/mimi/modules/scheduling/scheduling.go index 03331b4..0e52fbf 100644 --- a/cmd/mimi/modules/scheduling/scheduling.go +++ b/cmd/mimi/modules/scheduling/scheduling.go @@ -1,7 +1,7 @@ /* Package scheduling provides the scheduling module for Mimi. -This module allows users to CC Anise Robòta in their emails to schedule meetings and events. +This module allows authorized users to CC Nise in their emails to schedule meetings and events. When Nise is CCed in an email, she will parse the email and extract the date, time, and location of the event. Nise will then create a Google Calendar event and send an invitation. */ @@ -13,14 +13,17 @@ import ( _ "embed" "encoding/json" "fmt" - "net/http" + "log/slog" "text/template" "time" + "github.com/cenkalti/backoff/v4" "within.website/x/cmd/mimi/internal" "within.website/x/web/ollama" ) +//go:generate protoc --proto_path=. --go_out=. --go_opt=paths=source_relative --twirp_out=. scheduling.proto + func p[T any](t T) *T { return &t } @@ -30,30 +33,11 @@ const timeFormat string = "Monday January 2, 2006 at 3:04 PM" //go:embed nise_template.txt var niseTemplate string -type ConversationMember struct { - Role *string `json:"role,omitempty"` - Name string `json:"name"` - Email string `json:"email"` -} - -type NiseRequest struct { - Month string `json:"month"` - ConversationMembers []ConversationMember `json:"conversation_members"` - Message string `json:"message"` - Date string `json:"date"` -} - -type NiseResponse struct { - StartTime string `json:"start_time"` - Duration string `json:"duration"` - Summary string `json:"summary"` - Attendees []ConversationMember `json:"attendees"` - Location *string `json:"location,omitempty"` -} - type Module struct { cli *ollama.Client model string + + UnimplementedSchedulingServer } func New() *Module { @@ -63,46 +47,21 @@ func New() *Module { } } -func (m *Module) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - now := time.Now() - - // Only allow POST requests - if r.Method != http.MethodPost { - http.Error(w, "scheduling: only POST requests are allowed", http.StatusMethodNotAllowed) - return - } - - dateString := now.Format(timeFormat) - monthName := now.Month().String() - - var niseReq NiseRequest - if err := json.NewDecoder(r.Body).Decode(&niseReq); err != nil { - http.Error(w, fmt.Sprintf("scheduling: error decoding request: %v", err), http.StatusBadRequest) - return - } - - niseReq.Date = dateString - niseReq.Month = monthName +func (m *Module) ParseEmail(ctx context.Context, req *ParseReq) (*ParseResp, error) { + bo := backoff.NewExponentialBackOff() - resp, err := m.Handle(r.Context(), &niseReq) - if err != nil { - http.Error(w, fmt.Sprintf("scheduling: error handling request: %v", err), http.StatusInternalServerError) - return - } - - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, fmt.Sprintf("scheduling: error encoding response: %v", err), http.StatusInternalServerError) - return - } + return backoff.RetryNotifyWithData[*ParseResp](func() (*ParseResp, error) { + return m.parseEmail(ctx, req) + }, bo, func(err error, t time.Duration) { + slog.Error("error parsing email", "err", err, "t", t.String()) + }) } -func (m *Module) Handle(ctx context.Context, req *NiseRequest) (*NiseResponse, error) { +func (m *Module) parseEmail(ctx context.Context, req *ParseReq) (*ParseResp, error) { tmpl := template.Must(template.New("nise").Parse(niseTemplate)) var buf bytes.Buffer if err := tmpl.Execute(&buf, req); err != nil { - return nil, fmt.Errorf("scheduling: error executing template: %w", err) + return nil, backoff.Permanent(fmt.Errorf("scheduling: error executing template: %w", err)) } resp, err := m.cli.Chat(ctx, &ollama.CompleteRequest{ @@ -110,7 +69,7 @@ func (m *Module) Handle(ctx context.Context, req *NiseRequest) (*NiseResponse, e Messages: []ollama.Message{ { Role: "system", - Content: `You are Anise Robòta, a scheduling assistant. You have been CCed in an email to schedule a meeting. Your task is to read this email and extract the following information into a JSON object: + Content: `You are Nise, a scheduling assistant. You have been CCed in an email to schedule a meeting. Your task is to read this email and extract the following information into a JSON object: * The start time of the meeting * The duration of the meeting @@ -134,7 +93,7 @@ func (m *Module) Handle(ctx context.Context, req *NiseRequest) (*NiseResponse, e return nil, fmt.Errorf("scheduling: error summarizing email: %w", err) } - var niseResp NiseResponse + var niseResp ParseResp if err := json.Unmarshal([]byte(resp.Message.Content), &niseResp); err != nil { return nil, fmt.Errorf("scheduling: error unmarshaling response: %w", err) diff --git a/cmd/mimi/modules/scheduling/scheduling.pb.go b/cmd/mimi/modules/scheduling/scheduling.pb.go new file mode 100644 index 0000000..3f153f7 --- /dev/null +++ b/cmd/mimi/modules/scheduling/scheduling.pb.go @@ -0,0 +1,377 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.23.4 +// source: scheduling.proto + +package scheduling + +import ( + 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) +) + +type ConversationMember struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *ConversationMember) Reset() { + *x = ConversationMember{} + if protoimpl.UnsafeEnabled { + mi := &file_scheduling_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConversationMember) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConversationMember) ProtoMessage() {} + +func (x *ConversationMember) ProtoReflect() protoreflect.Message { + mi := &file_scheduling_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 ConversationMember.ProtoReflect.Descriptor instead. +func (*ConversationMember) Descriptor() ([]byte, []int) { + return file_scheduling_proto_rawDescGZIP(), []int{0} +} + +func (x *ConversationMember) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *ConversationMember) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ConversationMember) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type ParseReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Month string `protobuf:"bytes,1,opt,name=month,proto3" json:"month,omitempty"` + ConversationMembers []*ConversationMember `protobuf:"bytes,2,rep,name=conversation_members,json=conversationMembers,proto3" json:"conversation_members,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Date string `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` +} + +func (x *ParseReq) Reset() { + *x = ParseReq{} + if protoimpl.UnsafeEnabled { + mi := &file_scheduling_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ParseReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseReq) ProtoMessage() {} + +func (x *ParseReq) ProtoReflect() protoreflect.Message { + mi := &file_scheduling_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 ParseReq.ProtoReflect.Descriptor instead. +func (*ParseReq) Descriptor() ([]byte, []int) { + return file_scheduling_proto_rawDescGZIP(), []int{1} +} + +func (x *ParseReq) GetMonth() string { + if x != nil { + return x.Month + } + return "" +} + +func (x *ParseReq) GetConversationMembers() []*ConversationMember { + if x != nil { + return x.ConversationMembers + } + return nil +} + +func (x *ParseReq) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ParseReq) GetDate() string { + if x != nil { + return x.Date + } + return "" +} + +type ParseResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StartTime string `protobuf:"bytes,1,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + Duration string `protobuf:"bytes,2,opt,name=duration,proto3" json:"duration,omitempty"` + Summary string `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + Attendees []*ConversationMember `protobuf:"bytes,4,rep,name=attendees,proto3" json:"attendees,omitempty"` + Location string `protobuf:"bytes,5,opt,name=location,proto3" json:"location,omitempty"` +} + +func (x *ParseResp) Reset() { + *x = ParseResp{} + if protoimpl.UnsafeEnabled { + mi := &file_scheduling_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ParseResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseResp) ProtoMessage() {} + +func (x *ParseResp) ProtoReflect() protoreflect.Message { + mi := &file_scheduling_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 ParseResp.ProtoReflect.Descriptor instead. +func (*ParseResp) Descriptor() ([]byte, []int) { + return file_scheduling_proto_rawDescGZIP(), []int{2} +} + +func (x *ParseResp) GetStartTime() string { + if x != nil { + return x.StartTime + } + return "" +} + +func (x *ParseResp) GetDuration() string { + if x != nil { + return x.Duration + } + return "" +} + +func (x *ParseResp) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *ParseResp) GetAttendees() []*ConversationMember { + if x != nil { + return x.Attendees + } + return nil +} + +func (x *ParseResp) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +var File_scheduling_proto protoreflect.FileDescriptor + +var file_scheduling_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, + 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x6d, 0x69, 0x6d, 0x69, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x69, 0x6e, 0x67, 0x22, 0x52, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, + 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xb7, 0x01, 0x0a, 0x08, 0x50, 0x61, 0x72, + 0x73, 0x65, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x12, 0x67, 0x0a, 0x14, 0x63, + 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x77, 0x69, 0x74, 0x68, + 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x6d, 0x69, 0x6d, + 0x69, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x6f, 0x6e, + 0x76, 0x65, 0x72, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, + 0x13, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, + 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6e, 0x64, 0x65, + 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, + 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x6d, 0x69, 0x6d, 0x69, + 0x2e, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x6f, 0x6e, 0x76, + 0x65, 0x72, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x09, + 0x61, 0x74, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x6f, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x73, 0x0a, 0x0a, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x69, 0x6e, 0x67, 0x12, 0x65, 0x0a, 0x0a, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6d, 0x61, 0x69, + 0x6c, 0x12, 0x2a, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, + 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x6d, 0x69, 0x6d, 0x69, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, + 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x2b, 0x2e, + 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, + 0x2e, 0x6d, 0x69, 0x6d, 0x69, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x42, 0x2e, 0x5a, 0x2c, 0x77, 0x69, + 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2f, 0x78, 0x2f, 0x63, + 0x6d, 0x64, 0x2f, 0x6d, 0x69, 0x6d, 0x69, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x2f, + 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_scheduling_proto_rawDescOnce sync.Once + file_scheduling_proto_rawDescData = file_scheduling_proto_rawDesc +) + +func file_scheduling_proto_rawDescGZIP() []byte { + file_scheduling_proto_rawDescOnce.Do(func() { + file_scheduling_proto_rawDescData = protoimpl.X.CompressGZIP(file_scheduling_proto_rawDescData) + }) + return file_scheduling_proto_rawDescData +} + +var file_scheduling_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_scheduling_proto_goTypes = []interface{}{ + (*ConversationMember)(nil), // 0: within.website.x.mimi.scheduling.ConversationMember + (*ParseReq)(nil), // 1: within.website.x.mimi.scheduling.ParseReq + (*ParseResp)(nil), // 2: within.website.x.mimi.scheduling.ParseResp +} +var file_scheduling_proto_depIdxs = []int32{ + 0, // 0: within.website.x.mimi.scheduling.ParseReq.conversation_members:type_name -> within.website.x.mimi.scheduling.ConversationMember + 0, // 1: within.website.x.mimi.scheduling.ParseResp.attendees:type_name -> within.website.x.mimi.scheduling.ConversationMember + 1, // 2: within.website.x.mimi.scheduling.Scheduling.ParseEmail:input_type -> within.website.x.mimi.scheduling.ParseReq + 2, // 3: within.website.x.mimi.scheduling.Scheduling.ParseEmail:output_type -> within.website.x.mimi.scheduling.ParseResp + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_scheduling_proto_init() } +func file_scheduling_proto_init() { + if File_scheduling_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_scheduling_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConversationMember); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_scheduling_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_scheduling_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseResp); 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_scheduling_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_scheduling_proto_goTypes, + DependencyIndexes: file_scheduling_proto_depIdxs, + MessageInfos: file_scheduling_proto_msgTypes, + }.Build() + File_scheduling_proto = out.File + file_scheduling_proto_rawDesc = nil + file_scheduling_proto_goTypes = nil + file_scheduling_proto_depIdxs = nil +} diff --git a/cmd/mimi/modules/scheduling/scheduling.twirp.go b/cmd/mimi/modules/scheduling/scheduling.twirp.go new file mode 100644 index 0000000..22c9a99 --- /dev/null +++ b/cmd/mimi/modules/scheduling/scheduling.twirp.go @@ -0,0 +1,1113 @@ +// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. +// source: scheduling.proto + +package scheduling + +import context "context" +import fmt "fmt" +import http "net/http" +import io "io" +import json "encoding/json" +import strconv "strconv" +import strings "strings" + +import protojson "google.golang.org/protobuf/encoding/protojson" +import proto "google.golang.org/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +import bytes "bytes" +import errors "errors" +import path "path" +import url "net/url" + +// Version compatibility assertion. +// If the constant is not defined in the package, that likely means +// the package needs to be updated to work with this generated code. +// See https://twitchtv.github.io/twirp/docs/version_matrix.html +const _ = twirp.TwirpPackageMinVersion_8_1_0 + +// ==================== +// Scheduling Interface +// ==================== + +type Scheduling interface { + ParseEmail(context.Context, *ParseReq) (*ParseResp, error) +} + +// ========================== +// Scheduling Protobuf Client +// ========================== + +type schedulingProtobufClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewSchedulingProtobufClient creates a Protobuf client that implements the Scheduling interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewSchedulingProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Scheduling { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method> + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "within.website.x.mimi.scheduling", "Scheduling") + urls := [1]string{ + serviceURL + "ParseEmail", + } + + return &schedulingProtobufClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *schedulingProtobufClient) ParseEmail(ctx context.Context, in *ParseReq) (*ParseResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.mimi.scheduling") + ctx = ctxsetters.WithServiceName(ctx, "Scheduling") + ctx = ctxsetters.WithMethodName(ctx, "ParseEmail") + caller := c.callParseEmail + if c.interceptor != nil { + caller = func(ctx context.Context, req *ParseReq) (*ParseResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ParseReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ParseReq) when calling interceptor") + } + return c.callParseEmail(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ParseResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ParseResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *schedulingProtobufClient) callParseEmail(ctx context.Context, in *ParseReq) (*ParseResp, error) { + out := new(ParseResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ====================== +// Scheduling JSON Client +// ====================== + +type schedulingJSONClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewSchedulingJSONClient creates a JSON client that implements the Scheduling interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewSchedulingJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Scheduling { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method> + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "within.website.x.mimi.scheduling", "Scheduling") + urls := [1]string{ + serviceURL + "ParseEmail", + } + + return &schedulingJSONClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *schedulingJSONClient) ParseEmail(ctx context.Context, in *ParseReq) (*ParseResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.mimi.scheduling") + ctx = ctxsetters.WithServiceName(ctx, "Scheduling") + ctx = ctxsetters.WithMethodName(ctx, "ParseEmail") + caller := c.callParseEmail + if c.interceptor != nil { + caller = func(ctx context.Context, req *ParseReq) (*ParseResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ParseReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ParseReq) when calling interceptor") + } + return c.callParseEmail(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ParseResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ParseResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *schedulingJSONClient) callParseEmail(ctx context.Context, in *ParseReq) (*ParseResp, error) { + out := new(ParseResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ========================= +// Scheduling Server Handler +// ========================= + +type schedulingServer struct { + Scheduling + interceptor twirp.Interceptor + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names +} + +// NewSchedulingServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewSchedulingServer(svc Scheduling, opts ...interface{}) TwirpServer { + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + jsonCamelCase := false + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + return &schedulingServer{ + Scheduling: svc, + hooks: serverOpts.Hooks, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, + jsonCamelCase: jsonCamelCase, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *schedulingServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// handleRequestBodyError is used to handle error when the twirp server cannot read request +func (s *schedulingServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { + if context.Canceled == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) + return + } + if context.DeadlineExceeded == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) + return + } + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) +} + +// SchedulingPathPrefix is a convenience constant that may identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// with the default "/twirp" prefix and default CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const SchedulingPathPrefix = "/twirp/within.website.x.mimi.scheduling.Scheduling/" + +func (s *schedulingServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.mimi.scheduling") + ctx = ctxsetters.WithServiceName(ctx, "Scheduling") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: [<prefix>]/<package>.<Service>/<Method> + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "within.website.x.mimi.scheduling.Scheduling" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "ParseEmail": + s.serveParseEmail(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *schedulingServer) serveParseEmail(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveParseEmailJSON(ctx, resp, req) + case "application/protobuf": + s.serveParseEmailProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *schedulingServer) serveParseEmailJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "ParseEmail") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(ParseReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Scheduling.ParseEmail + if s.interceptor != nil { + handler = func(ctx context.Context, req *ParseReq) (*ParseResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ParseReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ParseReq) when calling interceptor") + } + return s.Scheduling.ParseEmail(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ParseResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ParseResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ParseResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ParseResp and nil error while calling ParseEmail. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *schedulingServer) serveParseEmailProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "ParseEmail") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(ParseReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } |
