Private
Public Access
1
0

prompt: adds ls, help, mark

This commit is contained in:
2023-06-23 00:32:17 -07:00
parent 191cffd4cf
commit 06046ac266
5 changed files with 209 additions and 80 deletions

84
main.go
View File

@@ -1,68 +1,68 @@
package main package main
import ( import (
"os" "flag"
"flag" "net/http"
"net/http" "os"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "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{ type LoggingHook struct {
prompt *prompt.Prompt prompt *prompt.Prompt
} }
func (t *LoggingHook) Run(e *zerolog.Event, level zerolog.Level, message string) { func (t *LoggingHook) Run(e *zerolog.Event, level zerolog.Level, message string) {
t.prompt.CleanAndRefreshForLogging() t.prompt.CleanAndRefreshForLogging()
} }
func setupLogging() { func setupLogging() {
debug := flag.Bool("debug", false, "enable debug logging") debug := flag.Bool("debug", false, "enable debug logging")
flag.Parse() flag.Parse()
// Pretty logging // Pretty logging
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
// Default level for this example is info, unless debug flag is present // Default level for this example is info, unless debug flag is present
zerolog.SetGlobalLevel(zerolog.InfoLevel) zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug { if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
} }
} }
func main() { func main() {
setupLogging() setupLogging()
c := web.MockHTTPServerConfiguration{ c := web.MockHTTPServerConfiguration{
AuthEnabled: false, AuthEnabled: false,
} }
addr := ":5738" addr := ":5738"
s := web.NewMockHTTPServer(c) s := web.NewMockHTTPServer(c)
httpServer := &http.Server{ httpServer := &http.Server{
Addr: addr, Addr: addr,
Handler: s, Handler: s,
} }
// Populate with test data // Populate with test data
s.Server.PopulateWithTestData() s.Server.PopulateWithTestData()
log.Info().Msgf("Generated test data. %d conversations", len(s.Server.Conversations())) log.Info().Msgf("Generated test data. %d conversations", len(s.Server.Conversations()))
log.Info().Msgf("Listening on %s", addr) log.Info().Msgf("Listening on %s", addr)
go httpServer.ListenAndServe() go httpServer.ListenAndServe()
rl := prompt.NewPrompt() rl := prompt.NewPrompt(&s.Server)
// Hook logging so we can refresh the prompt when something is logged. // Hook logging so we can refresh the prompt when something is logged.
log.Logger = log.Logger.Hook(&LoggingHook{prompt: rl}) log.Logger = log.Logger.Hook(&LoggingHook{prompt: rl})
// Read indefinitely // Read indefinitely
err := rl.StartInteractive() err := rl.StartInteractive()
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
} }
} }

View File

@@ -1,6 +1,11 @@
package model package model
import "time" import (
"strings"
"time"
"github.com/rs/zerolog"
)
type Conversation struct { type Conversation struct {
Date time.Time `json:"date"` Date time.Time `json:"date"`
@@ -10,3 +15,22 @@ type Conversation struct {
LastMessagePreview string `json:"lastMessagePreview"` LastMessagePreview string `json:"lastMessagePreview"`
Guid string `json:"guid"` 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)
}
}

View File

@@ -1,63 +1,158 @@
package prompt package prompt
import ( import (
"io" "fmt"
"io"
"strings"
"github.com/rs/zerolog/log" "code.severnaya.net/kordophone-mock/v2/server"
"github.com/chzyer/readline" "github.com/chzyer/readline"
"github.com/rs/zerolog/log"
) )
type Prompt struct { type Prompt struct {
rl *readline.Instance rl *readline.Instance
server *server.Server
} }
func NewPrompt() *Prompt { func (p *Prompt) listConversations() {
rl, err := readline.NewEx(&readline.Config{ 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 ", Prompt: "\033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp", HistoryFile: "/tmp/readline.tmp",
InterruptPrompt: "^C", InterruptPrompt: "^C",
EOFPrompt: "exit", EOFPrompt: "exit",
AutoComplete: completer,
HistorySearchFold: true, HistorySearchFold: true,
}) })
if err != nil { if err != nil {
panic(err) panic(err)
} }
return &Prompt{ return &Prompt{
rl: rl, rl: rl,
} server: server,
}
} }
func (p *Prompt) StartInteractive() error { func (p *Prompt) StartInteractive() error {
for { for {
line, err := p.rl.Readline() line, err := p.rl.Readline()
if err == readline.ErrInterrupt { if err == readline.ErrInterrupt {
if len(line) == 0 { if len(line) == 0 {
break break
} else { } else {
continue continue
} }
} else if err == io.EOF { } else if err == io.EOF {
break break
} }
switch { line = strings.TrimSpace(line)
case line == "exit":
return nil
default:
log.Info().Msgf("Line: %s", 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] <guid>")
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 <guid> list messages for conversation")
fmt.Println("\tmark [-r] <guid> 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() { func (p *Prompt) CleanAndRefreshForLogging() {
p.rl.Clean() p.rl.Clean()
// xxx: Lazy hack to make sure this runs _after_ the log is written. // xxx: Lazy hack to make sure this runs _after_ the log is written.
go p.rl.Refresh() go p.rl.Refresh()
} }

View File

@@ -54,11 +54,21 @@ func (s *Server) Conversations() []model.Conversation {
return s.conversations 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) { func (s *Server) ConversationForGUID(guid string) (*model.Conversation, error) {
var conversation *model.Conversation = nil var conversation *model.Conversation = nil
for _, c := range s.conversations { for i := range s.conversations {
c := &s.conversations[i]
if c.Guid == guid { if c.Guid == guid {
conversation = &c conversation = c
break break
} }
} }
@@ -119,7 +129,7 @@ func (s *Server) CheckBearerToken(token string) bool {
return s.authenticateToken(token) 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] messages := s.messageStore[conversation.Guid]
sort.Slice(messages, func(i int, j int) bool { sort.Slice(messages, func(i int, j int) bool {
return messages[i].Date.Before(messages[j].Date) return messages[i].Date.Before(messages[j].Date)

View File

@@ -104,7 +104,7 @@ func (m *MockHTTPServer) handleMessages(w http.ResponseWriter, r *http.Request)
return return
} }
messages := m.Server.MessagesForConversation(*conversation) messages := m.Server.MessagesForConversation(conversation)
jsonData, err := json.Marshal(messages) jsonData, err := json.Marshal(messages)
if err != nil { if err != nil {