diff --git a/main.go b/main.go index b19d73e..681141f 100644 --- a/main.go +++ b/main.go @@ -1,68 +1,68 @@ package main import ( - "os" - "flag" - "net/http" + "flag" + "net/http" + "os" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" - "code.severnaya.net/kordophone-mock/v2/web" - "code.severnaya.net/kordophone-mock/v2/prompt" + "code.severnaya.net/kordophone-mock/v2/prompt" + "code.severnaya.net/kordophone-mock/v2/web" ) -type LoggingHook struct{ - prompt *prompt.Prompt +type LoggingHook struct { + prompt *prompt.Prompt } func (t *LoggingHook) Run(e *zerolog.Event, level zerolog.Level, message string) { - t.prompt.CleanAndRefreshForLogging() + t.prompt.CleanAndRefreshForLogging() } func setupLogging() { - debug := flag.Bool("debug", false, "enable debug logging") - flag.Parse() + debug := flag.Bool("debug", false, "enable debug logging") + flag.Parse() - // Pretty logging - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Pretty logging + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - // Default level for this example is info, unless debug flag is present - zerolog.SetGlobalLevel(zerolog.InfoLevel) - if *debug { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - } + // Default level for this example is info, unless debug flag is present + zerolog.SetGlobalLevel(zerolog.InfoLevel) + if *debug { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } } func main() { - setupLogging() + setupLogging() - c := web.MockHTTPServerConfiguration{ - AuthEnabled: false, - } + c := web.MockHTTPServerConfiguration{ + AuthEnabled: false, + } - addr := ":5738" - s := web.NewMockHTTPServer(c) - httpServer := &http.Server{ - Addr: addr, - Handler: s, - } + addr := ":5738" + s := web.NewMockHTTPServer(c) + httpServer := &http.Server{ + Addr: addr, + Handler: s, + } - // Populate with test data - s.Server.PopulateWithTestData() - log.Info().Msgf("Generated test data. %d conversations", len(s.Server.Conversations())) + // Populate with test data + s.Server.PopulateWithTestData() + log.Info().Msgf("Generated test data. %d conversations", len(s.Server.Conversations())) - log.Info().Msgf("Listening on %s", addr) - go httpServer.ListenAndServe() + log.Info().Msgf("Listening on %s", addr) + go httpServer.ListenAndServe() - rl := prompt.NewPrompt() + rl := prompt.NewPrompt(&s.Server) - // Hook logging so we can refresh the prompt when something is logged. - log.Logger = log.Logger.Hook(&LoggingHook{prompt: rl}) + // Hook logging so we can refresh the prompt when something is logged. + log.Logger = log.Logger.Hook(&LoggingHook{prompt: rl}) - // Read indefinitely - err := rl.StartInteractive() - if err != nil { - log.Error().Err(err) - } + // Read indefinitely + err := rl.StartInteractive() + if err != nil { + log.Error().Err(err) + } } diff --git a/model/conversation.go b/model/conversation.go index 9f4d326..f99f301 100644 --- a/model/conversation.go +++ b/model/conversation.go @@ -1,6 +1,11 @@ package model -import "time" +import ( + "strings" + "time" + + "github.com/rs/zerolog" +) type Conversation struct { Date time.Time `json:"date"` @@ -10,3 +15,22 @@ type Conversation struct { LastMessagePreview string `json:"lastMessagePreview"` Guid string `json:"guid"` } + +func (c *Conversation) GetDisplayName() string { + if c.DisplayName == nil { + return strings.Join(c.Participants, ",") + } + + return *c.DisplayName +} + +func (c Conversation) MarshalZerologObject(e *zerolog.Event) { + e.Str("guid", c.Guid) + e.Time("date", c.Date) + e.Int("unreadCount", c.UnreadCount) + e.Str("lastMessagePreview", c.LastMessagePreview) + e.Strs("participants", c.Participants) + if c.DisplayName != nil { + e.Str("displayName", *c.DisplayName) + } +} diff --git a/prompt/prompt.go b/prompt/prompt.go index d04f78c..1ec014f 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -1,63 +1,158 @@ package prompt import ( - "io" + "fmt" + "io" + "strings" - "github.com/rs/zerolog/log" - "github.com/chzyer/readline" + "code.severnaya.net/kordophone-mock/v2/server" + "github.com/chzyer/readline" + "github.com/rs/zerolog/log" ) type Prompt struct { - rl *readline.Instance + rl *readline.Instance + server *server.Server } -func NewPrompt() *Prompt { - rl, err := readline.NewEx(&readline.Config{ +func (p *Prompt) listConversations() { + conversations := p.server.SortedConversations() + for _, c := range conversations { + fmt.Printf("%s %s \t %s ", c.Guid, c.GetDisplayName(), c.Date.Format("2006-01-02 15:04:05")) + if c.UnreadCount > 0 { + fmt.Printf("(%d unread)", c.UnreadCount) + } + + fmt.Println() + } +} + +func (p *Prompt) listMessages(guid string) { + conversation, err := p.server.ConversationForGUID(guid) + if err != nil { + log.Err(err).Msgf("Error listing messages for conversation %s", guid) + return + } + + messages := p.server.MessagesForConversation(conversation) + for _, m := range messages { + var sender string + if m.Sender == nil { + sender = "(Me)" + } else { + sender = *m.Sender + } + + fmt.Printf("%s %s From: %s\n", m.Guid, m.Date.Format("2006-01-02 15:04:05"), sender) + fmt.Printf("\t %s\n", m.Text) + } +} + +func (p *Prompt) markConversation(guid string, read bool) { + conversation, err := p.server.ConversationForGUID(guid) + if err != nil { + log.Err(err).Msgf("Error marking conversation %s as read", guid) + return + } + + if read { + conversation.UnreadCount = 0 + } else { + conversation.UnreadCount = 1 + } +} + +func NewPrompt(server *server.Server) *Prompt { + completer := readline.NewPrefixCompleter( + readline.PcItem("ls"), + readline.PcItem("mark", + readline.PcItem("-r"), + ), + readline.PcItem("help"), + readline.PcItem("exit"), + ) + + rl, err := readline.NewEx(&readline.Config{ Prompt: "\033[31m»\033[0m ", HistoryFile: "/tmp/readline.tmp", InterruptPrompt: "^C", EOFPrompt: "exit", + AutoComplete: completer, - HistorySearchFold: true, + HistorySearchFold: true, }) if err != nil { panic(err) } - return &Prompt{ - rl: rl, - } + return &Prompt{ + rl: rl, + server: server, + } } func (p *Prompt) StartInteractive() error { - for { + for { line, err := p.rl.Readline() - if err == readline.ErrInterrupt { - if len(line) == 0 { - break - } else { - continue - } - } else if err == io.EOF { - break - } + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } - switch { - case line == "exit": - return nil - default: - log.Info().Msgf("Line: %s", line) - } - } + line = strings.TrimSpace(line) - return nil + switch { + case strings.HasPrefix(line, "ls"): // List + args := strings.Split(line, " ") + + if len(args) == 1 { + p.listConversations() + } else { + p.listMessages(args[1]) + } + + case strings.HasPrefix(line, "mark"): // Mark + args := strings.Split(line, " ") + if len(args) < 2 { + log.Info().Msgf("Usage: mark [-r] ") + continue + } + + read := false + if args[1] == "-r" { + read = true + args = args[1:] + } + + p.markConversation(args[1], read) + + case line == "help": // Help + fmt.Println("Commands:") + fmt.Println("\tls list conversations") + fmt.Println("\tls list messages for conversation") + fmt.Println("\tmark [-r] mark conversation as unread/[r]ead") + fmt.Println("\texit exits the program") + + case line == "exit": // Exit + return nil + + default: + fmt.Printf("Unknown command: %s\n", line) + } + } + + return nil } func (p *Prompt) CleanAndRefreshForLogging() { - p.rl.Clean() - - // xxx: Lazy hack to make sure this runs _after_ the log is written. - go p.rl.Refresh() -} + p.rl.Clean() + // xxx: Lazy hack to make sure this runs _after_ the log is written. + go p.rl.Refresh() +} diff --git a/server/server.go b/server/server.go index 4722617..830edb8 100644 --- a/server/server.go +++ b/server/server.go @@ -54,11 +54,21 @@ func (s *Server) Conversations() []model.Conversation { return s.conversations } +func (s *Server) SortedConversations() []model.Conversation { + conversations := s.Conversations() + sort.Slice(conversations, func(i, j int) bool { + return conversations[i].Date.After(conversations[j].Date) + }) + + return conversations +} + func (s *Server) ConversationForGUID(guid string) (*model.Conversation, error) { var conversation *model.Conversation = nil - for _, c := range s.conversations { + for i := range s.conversations { + c := &s.conversations[i] if c.Guid == guid { - conversation = &c + conversation = c break } } @@ -119,7 +129,7 @@ func (s *Server) CheckBearerToken(token string) bool { return s.authenticateToken(token) } -func (s *Server) MessagesForConversation(conversation model.Conversation) []model.Message { +func (s *Server) MessagesForConversation(conversation *model.Conversation) []model.Message { messages := s.messageStore[conversation.Guid] sort.Slice(messages, func(i int, j int) bool { return messages[i].Date.Before(messages[j].Date) diff --git a/web/server.go b/web/server.go index 083f542..37f7768 100644 --- a/web/server.go +++ b/web/server.go @@ -104,7 +104,7 @@ func (m *MockHTTPServer) handleMessages(w http.ResponseWriter, r *http.Request) return } - messages := m.Server.MessagesForConversation(*conversation) + messages := m.Server.MessagesForConversation(conversation) jsonData, err := json.Marshal(messages) if err != nil {