diff --git a/channel.go b/channel.go index de626b2..5216dd9 100644 --- a/channel.go +++ b/channel.go @@ -31,6 +31,7 @@ type Channel struct { send chan cchat.SendableMessage // ideally this should be another type edit chan Message // id del chan MessageHeader + typ chan Author messageMutex sync.Mutex messages map[uint32]Message @@ -44,13 +45,14 @@ type Channel struct { } var ( - _ cchat.Server = (*Channel)(nil) - _ cchat.ServerMessage = (*Channel)(nil) - _ cchat.ServerMessageSender = (*Channel)(nil) - _ cchat.ServerMessageSendCompleter = (*Channel)(nil) - _ cchat.ServerNickname = (*Channel)(nil) - _ cchat.ServerMessageEditor = (*Channel)(nil) - _ cchat.ServerMessageActioner = (*Channel)(nil) + _ cchat.Server = (*Channel)(nil) + _ cchat.ServerMessage = (*Channel)(nil) + _ cchat.ServerMessageSender = (*Channel)(nil) + _ cchat.ServerMessageSendCompleter = (*Channel)(nil) + _ cchat.ServerNickname = (*Channel)(nil) + _ cchat.ServerMessageEditor = (*Channel)(nil) + _ cchat.ServerMessageActioner = (*Channel)(nil) + _ cchat.ServerMessageTypingIndicator = (*Channel)(nil) ) func (ch *Channel) ID() string { @@ -93,6 +95,7 @@ func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) ( ch.send = make(chan cchat.SendableMessage) ch.edit = make(chan Message) ch.del = make(chan MessageHeader) + ch.typ = make(chan Author) // Generate the backlog. for i := 0; i < FetchBacklog; i++ { @@ -330,9 +333,10 @@ func (ch *Channel) SendMessage(msg cchat.SendableMessage) error { } const ( - DeleteAction = "Delete" - NoopAction = "No-op" - BestTrapAction = "What's the best trap?" + DeleteAction = "Delete" + NoopAction = "No-op" + BestTrapAction = "What's the best trap?" + TriggerTypingAction = "Trigger Typing" ) func (ch *Channel) MessageActions(id string) []string { @@ -347,7 +351,7 @@ func (ch *Channel) MessageActions(id string) []string { // takes a container: the frontend should call this in a goroutine. func (ch *Channel) DoMessageAction(action, messageID string) error { switch action { - case DeleteAction: + case DeleteAction, TriggerTypingAction: i, err := strconv.Atoi(messageID) if err != nil { return errors.Wrap(err, "Invalid ID") @@ -355,7 +359,14 @@ func (ch *Channel) DoMessageAction(action, messageID string) error { // Simulate IO. simulateAustralianInternet() - ch.del <- MessageHeader{uint32(i), time.Now()} + + switch action { + case DeleteAction: + ch.del <- MessageHeader{uint32(i), time.Now()} + case TriggerTypingAction: + // Find the message. + ch.typ <- Author{name: ch.messages[uint32(i)].author} + } case NoopAction: // do nothing. @@ -439,6 +450,49 @@ func lookbackCheck(words []string, i int, prev, this string) bool { return strings.HasPrefix(this, words[i]) && i > 0 && words[i-1] == prev } +// Typing sleeps and returns possibly an error. +func (ch *Channel) Typing() error { + return simulateAustralianInternet() +} + +// TypingTimeout returns 5 seconds. +func (ch *Channel) TypingTimeout() time.Duration { + return 5 * time.Second +} + +type Typer struct { + Author + time time.Time +} + +var _ cchat.Typer = (*Typer)(nil) + +func newTyper(a Author) *Typer { return &Typer{a, time.Now()} } +func randomTyper() *Typer { return &Typer{randomAuthor(), time.Now()} } +func (t *Typer) Time() time.Time { return t.time } + +func (ch *Channel) TypingSubscribe(ti cchat.TypingIndicator) (stop func(), err error) { + var stopch = make(chan struct{}) + + go func() { + var ticker = time.NewTicker(8 * time.Second) + defer ticker.Stop() + + for { + select { + case <-stopch: + return + case <-ticker.C: + ti.AddTyper(randomTyper()) + case author := <-ch.typ: + ti.AddTyper(newTyper(author)) + } + } + }() + + return func() { close(stopch) }, nil +} + func generateChannels(s *Session, amount int) []cchat.Server { var channels = make([]cchat.Server, amount) for i := range channels { @@ -459,30 +513,3 @@ func generateChannels(s *Session, amount int) []cchat.Server { func randClamp(min, max int) int { return rand.Intn(max-min) + min } - -// ErrTimedOut is returned when the simulated IO decides to fail. -var ErrTimedOut = errors.New("Australian Internet unsupported.") - -// simulate network latency -func simulateAustralianInternet() error { - return simulateAustralianInternetCtx(context.Background()) -} - -func simulateAustralianInternetCtx(ctx context.Context) (err error) { - var ms = randClamp(internetMinLatency, internetMaxLatency) - - select { - case <-time.After(time.Duration(ms) * time.Millisecond): - // noop - case <-ctx.Done(): - return ctx.Err() - } - - // because australia, drop packet 20% of the time if internetCanFail is - // true. - if internetCanFail && rand.Intn(100) < 20 { - return ErrTimedOut - } - - return nil -} diff --git a/internet.go b/internet.go new file mode 100644 index 0000000..6c2b25b --- /dev/null +++ b/internet.go @@ -0,0 +1,44 @@ +package mock + +import ( + "context" + "math/rand" + "time" + + "github.com/pkg/errors" +) + +var ( + // channel.go @ simulateAustralianInternet + internetCanFail = true + // 500ms ~ 3s + internetMinLatency = 500 + internetMaxLatency = 3000 +) + +// ErrTimedOut is returned when the simulated IO decides to fail. +var ErrTimedOut = errors.New("Australian Internet unsupported.") + +// simulate network latency +func simulateAustralianInternet() error { + return simulateAustralianInternetCtx(context.Background()) +} + +func simulateAustralianInternetCtx(ctx context.Context) (err error) { + var ms = randClamp(internetMinLatency, internetMaxLatency) + + select { + case <-time.After(time.Duration(ms) * time.Millisecond): + // noop + case <-ctx.Done(): + return ctx.Err() + } + + // because australia, drop packet 20% of the time if internetCanFail is + // true. + if internetCanFail && rand.Intn(100) < 20 { + return ErrTimedOut + } + + return nil +} diff --git a/message.go b/message.go index 46b8bbb..985b27a 100644 --- a/message.go +++ b/message.go @@ -79,12 +79,7 @@ func echoMessage(sendable cchat.SendableMessage, id uint32, author text.Rich) Me } func randomMessage(id uint32) Message { - var author = randomdata.SillyName() - - return randomMessageWithAuthor(id, text.Rich{ - Content: author, - Segments: []text.Segment{segments.NewRandomColored(author)}, - }) + return randomMessageWithAuthor(id, randomAuthor().name) } func randomMessageWithAuthor(id uint32, author text.Rich) Message { @@ -122,6 +117,16 @@ var ( _ cchat.MessageAuthorAvatar = (*Author)(nil) ) +func randomAuthor() Author { + var author = randomdata.SillyName() + return Author{ + name: text.Rich{ + Content: author, + Segments: []text.Segment{segments.NewRandomColored(author)}, + }, + } +} + func (a Author) ID() string { return a.name.Content } diff --git a/service.go b/service.go index d591032..736f5f8 100644 --- a/service.go +++ b/service.go @@ -82,16 +82,9 @@ func (Authenticator) Authenticate(form []string) (cchat.Session, error) { return newSession(form[0]), nil } -var ( - // channel.go @ simulateAustralianInternet - internetCanFail = true - // 500ms ~ 3s - internetMinLatency = 500 - internetMaxLatency = 3000 -) - func (s Service) Configuration() (map[string]string, error) { return map[string]string{ + // refer to internet.go "internet.canFail": strconv.FormatBool(internetCanFail), "internet.minLatency": strconv.Itoa(internetMinLatency), "internet.maxLatency": strconv.Itoa(internetMaxLatency),