cchat-mock/channel.go

517 lines
12 KiB
Go
Raw Normal View History

2020-05-20 07:13:12 +00:00
package mock
import (
"context"
2020-05-20 07:13:12 +00:00
"math/rand"
2020-05-20 21:23:44 +00:00
"strconv"
2020-05-22 20:57:35 +00:00
"strings"
2020-06-08 22:19:13 +00:00
"sync"
2020-05-20 07:13:12 +00:00
"sync/atomic"
"time"
"github.com/Pallinder/go-randomdata"
"github.com/diamondburned/cchat"
2020-06-04 04:36:46 +00:00
"github.com/diamondburned/cchat-mock/segments"
"github.com/diamondburned/cchat/text"
2020-06-08 22:19:13 +00:00
"github.com/pkg/errors"
2020-05-20 07:13:12 +00:00
)
2020-06-08 22:19:13 +00:00
// FetchBacklog is the number of messages to fake-fetch.
const FetchBacklog = 35
2020-06-09 03:58:12 +00:00
const maxBacklog = FetchBacklog * 2
2020-06-08 22:19:13 +00:00
// max number to add to before the next author, with rand.Intn(limit) + incr.
const sameAuthorLimit = 12
2020-06-04 04:36:46 +00:00
2020-05-20 07:13:12 +00:00
type Channel struct {
2020-06-04 04:36:46 +00:00
id uint32
name string
username text.Rich
2020-06-08 22:19:13 +00:00
send chan cchat.SendableMessage // ideally this should be another type
edit chan Message // id
2020-06-15 01:57:02 +00:00
del chan MessageHeader
2020-07-03 23:53:46 +00:00
typ chan Author
2020-06-08 22:19:13 +00:00
messageMutex sync.Mutex
messages map[uint32]Message
messageids []uint32 // indices
2020-06-08 22:19:13 +00:00
// used for unique ID generation of messages
incrID uint32
// used for generating the same author multiple times before shuffling, goes
// up to about 12 or so. check sameAuthorLimit.
incrAuthor uint8
2020-05-20 07:13:12 +00:00
}
var (
2020-07-03 23:53:46 +00:00
_ 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)
2020-05-20 07:13:12 +00:00
)
2020-05-20 21:23:44 +00:00
func (ch *Channel) ID() string {
return strconv.Itoa(int(ch.id))
}
2020-06-09 03:58:12 +00:00
func (ch *Channel) Name() text.Rich {
return text.Rich{Content: ch.name}
2020-06-04 04:36:46 +00:00
}
// Nickname sets the labeler to the nickname. It simulates heavy IO. This
// function stops as cancel is called in JoinServer, as Nickname is specially
// for that.
2020-06-15 01:57:02 +00:00
//
// The given context is cancelled.
2020-06-29 21:01:21 +00:00
func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) {
// Simulate IO with cancellation. Ignore the error if it's a simulated time
// out, else return.
if err := simulateAustralianInternetCtx(ctx); err != nil && err != ErrTimedOut {
2020-06-29 21:01:21 +00:00
return nil, err
}
2020-06-09 03:58:12 +00:00
2020-06-04 04:36:46 +00:00
labeler.SetLabel(ch.username)
2020-06-29 21:01:21 +00:00
return func() {}, nil
2020-05-20 07:13:12 +00:00
}
2020-06-15 01:57:02 +00:00
func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
2020-06-08 22:19:13 +00:00
// Is this a fresh channel? If yes, generate messages with some IO latency.
if len(ch.messageids) == 0 || ch.messages == nil {
2020-06-12 18:21:12 +00:00
// Simulate IO and error.
2020-06-15 01:57:02 +00:00
if err := simulateAustralianInternetCtx(ctx); err != nil {
2020-06-12 18:21:12 +00:00
return nil, err
}
2020-06-03 23:13:06 +00:00
2020-06-08 22:19:13 +00:00
// Initialize.
ch.messages = make(map[uint32]Message, FetchBacklog)
ch.messageids = make([]uint32, 0, FetchBacklog)
2020-06-15 01:57:02 +00:00
// Allocate 3 channels that we won't clean up, because we're lazy.
2020-06-08 22:19:13 +00:00
ch.send = make(chan cchat.SendableMessage)
ch.edit = make(chan Message)
2020-06-15 01:57:02 +00:00
ch.del = make(chan MessageHeader)
2020-07-03 23:53:46 +00:00
ch.typ = make(chan Author)
2020-06-08 22:19:13 +00:00
// Generate the backlog.
for i := 0; i < FetchBacklog; i++ {
2020-06-15 01:57:02 +00:00
ch.addMessage(ch.randomMsg(), ct)
2020-06-08 22:19:13 +00:00
}
2020-06-09 03:58:12 +00:00
} else {
// Else, flush the old backlog over.
for i := range ch.messages {
2020-06-15 01:57:02 +00:00
ct.CreateMessage(ch.messages[i])
2020-06-09 03:58:12 +00:00
}
2020-05-20 07:13:12 +00:00
}
2020-06-15 01:57:02 +00:00
// Initialize context for cancellation. The context passed in is used only
// for initialization, so we'll use our own context for the loop.
ctx, stop := context.WithCancel(context.Background())
2020-06-03 23:13:06 +00:00
2020-05-20 07:13:12 +00:00
go func() {
2020-06-03 23:13:06 +00:00
ticker := time.NewTicker(4 * time.Second)
defer ticker.Stop()
editTick := time.NewTicker(10 * time.Second)
defer editTick.Stop()
// deleteTick := time.NewTicker(15 * time.Second)
// defer deleteTick.Stop()
2020-06-03 23:13:06 +00:00
2020-05-20 07:13:12 +00:00
for {
select {
case msg := <-ch.send:
2020-06-15 01:57:02 +00:00
ch.addMessage(echoMessage(msg, ch.nextID(), ch.username), ct)
2020-06-08 22:19:13 +00:00
case msg := <-ch.edit:
2020-06-15 01:57:02 +00:00
ct.UpdateMessage(msg)
case msh := <-ch.del:
ch.deleteMessage(msh, ct)
2020-06-08 22:19:13 +00:00
2020-06-03 23:13:06 +00:00
case <-ticker.C:
2020-06-15 01:57:02 +00:00
ch.addMessage(ch.randomMsg(), ct)
2020-06-08 22:19:13 +00:00
2020-06-03 23:13:06 +00:00
case <-editTick.C:
2020-06-08 22:19:13 +00:00
var old = ch.randomOldMsg()
2020-06-15 01:57:02 +00:00
ch.updateMessage(newRandomMessage(old.id, old.author), ct)
2020-06-08 22:19:13 +00:00
// case <-deleteTick.C:
// var old = ch.randomOldMsg()
// ch.deleteMessage(MessageHeader{old.id, time.Now()}, container)
2020-06-08 22:19:13 +00:00
2020-06-15 01:57:02 +00:00
case <-ctx.Done():
2020-05-20 07:13:12 +00:00
return
}
}
}()
2020-06-15 01:57:02 +00:00
return stop, nil
2020-05-20 07:13:12 +00:00
}
2020-06-28 06:39:12 +00:00
// MessageEditable returns true if the message belongs to the author.
func (ch *Channel) MessageEditable(id string) bool {
i, err := parseID(id)
if err != nil {
return false
}
ch.messageMutex.Lock()
defer ch.messageMutex.Unlock()
m, ok := ch.messages[i]
if ok {
// Editable if same author.
return m.author.Content == ch.username.Content
}
return false
}
2020-06-08 22:19:13 +00:00
func (ch *Channel) RawMessageContent(id string) (string, error) {
2020-06-09 03:58:12 +00:00
i, err := parseID(id)
if err != nil {
return "", err
}
2020-06-08 22:19:13 +00:00
ch.messageMutex.Lock()
defer ch.messageMutex.Unlock()
m, ok := ch.messages[i]
if ok {
return m.content, nil
2020-06-08 22:19:13 +00:00
}
return "", errors.New("Message not found")
2020-06-08 22:19:13 +00:00
}
func (ch *Channel) EditMessage(id, content string) error {
2020-06-09 03:58:12 +00:00
i, err := parseID(id)
if err != nil {
return err
}
simulateAustralianInternet()
2020-06-08 22:19:13 +00:00
ch.messageMutex.Lock()
defer ch.messageMutex.Unlock()
m, ok := ch.messages[i]
if ok {
m.content = content
ch.messages[i] = m
ch.edit <- m
2020-06-08 22:19:13 +00:00
return nil
}
2020-06-08 22:19:13 +00:00
return errors.New("Message not found.")
2020-05-20 07:13:12 +00:00
}
2020-06-08 22:19:13 +00:00
func (ch *Channel) addMessage(msg Message, container cchat.MessagesContainer) {
ch.messageMutex.Lock()
// Clean up the backlog.
2020-06-09 03:58:12 +00:00
if clean := len(ch.messages) - maxBacklog; clean > 0 {
// Remove them from the map.
for _, id := range ch.messageids[:clean] {
delete(ch.messages, id)
2020-06-09 03:58:12 +00:00
}
2020-06-08 22:19:13 +00:00
2020-06-09 03:58:12 +00:00
// Cut the message IDs away by shifting the slice.
ch.messageids = append(ch.messageids[:0], ch.messageids[clean:]...)
2020-06-08 22:19:13 +00:00
}
2020-06-09 03:58:12 +00:00
ch.messages[msg.id] = msg
ch.messageids = append(ch.messageids, msg.id)
2020-06-09 03:58:12 +00:00
ch.messageMutex.Unlock()
2020-06-08 22:19:13 +00:00
container.CreateMessage(msg)
}
func (ch *Channel) updateMessage(msg Message, container cchat.MessagesContainer) {
ch.messageMutex.Lock()
_, ok := ch.messages[msg.id]
2020-06-09 03:58:12 +00:00
if ok {
ch.messages[msg.id] = msg
2020-06-08 22:19:13 +00:00
}
2020-06-09 03:58:12 +00:00
ch.messageMutex.Unlock()
if ok {
container.UpdateMessage(msg)
}
2020-06-08 22:19:13 +00:00
}
func (ch *Channel) deleteMessage(msg MessageHeader, container cchat.MessagesContainer) {
ch.messageMutex.Lock()
// Delete from the map.
delete(ch.messages, msg.id)
// Delete from the ordered slice.
var ok bool
for i, id := range ch.messageids {
if id == msg.id {
ch.messageids = append(ch.messageids[:i], ch.messageids[i+1:]...)
ok = true
break
}
2020-06-08 22:19:13 +00:00
}
2020-06-09 03:58:12 +00:00
ch.messageMutex.Unlock()
if ok {
container.DeleteMessage(msg)
}
2020-06-08 22:19:13 +00:00
}
// randomMsgID returns a random recent message ID.
func (ch *Channel) randomOldMsg() Message {
ch.messageMutex.Lock()
defer ch.messageMutex.Unlock()
// Pick a random index from last, clamped to 10 and len channel.
2020-06-13 00:34:44 +00:00
n := len(ch.messageids) - 1 - rand.Intn(len(ch.messageids))%10
return ch.messages[ch.messageids[n]]
2020-06-08 22:19:13 +00:00
}
// randomMsg uses top of the state algorithms to return fair and balanced
// messages suitable for rigorous testing.
func (ch *Channel) randomMsg() (msg Message) {
ch.messageMutex.Lock()
defer ch.messageMutex.Unlock()
// If we don't have any messages, then skip.
if len(ch.messages) == 0 {
return randomMessage(ch.nextID())
}
// Add a random number into incrAuthor and determine if that should be
// enough to generate a new author.
ch.incrAuthor += uint8(rand.Intn(5)) // 2~4 appearances
var lastID = ch.messageids[len(ch.messageids)-1]
var last = ch.messages[lastID]
// If the last author is not the current user, then we can use it.
// Should we generate a new author for the new message? No if we're not over
// the limits.
if last.author.Content != ch.username.Content && ch.incrAuthor < sameAuthorLimit {
msg = randomMessageWithAuthor(ch.nextID(), last.author)
} else {
2020-06-08 22:19:13 +00:00
msg = randomMessage(ch.nextID())
2020-06-09 03:58:12 +00:00
ch.incrAuthor = 0 // reset
2020-06-08 22:19:13 +00:00
}
return
}
func (ch *Channel) nextID() (id uint32) {
return atomic.AddUint32(&ch.incrID, 1)
}
2020-05-20 07:13:12 +00:00
func (ch *Channel) SendMessage(msg cchat.SendableMessage) error {
if err := simulateAustralianInternet(); err != nil {
return errors.Wrap(err, "Failed to send message")
2020-05-20 07:13:12 +00:00
}
2020-06-03 23:13:06 +00:00
go func() {
// Make no guarantee that a message may arrive immediately when the
// function exits.
<-time.After(time.Second)
ch.send <- msg
}()
2020-05-20 07:13:12 +00:00
return nil
}
2020-06-08 22:19:13 +00:00
const (
2020-07-03 23:53:46 +00:00
DeleteAction = "Delete"
NoopAction = "No-op"
BestTrapAction = "What's the best trap?"
TriggerTypingAction = "Trigger Typing"
2020-06-08 22:19:13 +00:00
)
2020-06-20 23:14:23 +00:00
func (ch *Channel) MessageActions(id string) []string {
2020-06-08 22:19:13 +00:00
return []string{
DeleteAction,
NoopAction,
BestTrapAction,
2020-07-03 23:55:27 +00:00
TriggerTypingAction,
2020-06-08 22:19:13 +00:00
}
}
// DoMessageAction will be blocked by IO. As goes for every other method that
// takes a container: the frontend should call this in a goroutine.
2020-06-15 01:57:02 +00:00
func (ch *Channel) DoMessageAction(action, messageID string) error {
2020-06-08 22:19:13 +00:00
switch action {
2020-07-03 23:53:46 +00:00
case DeleteAction, TriggerTypingAction:
2020-06-08 22:19:13 +00:00
i, err := strconv.Atoi(messageID)
if err != nil {
return errors.Wrap(err, "Invalid ID")
}
2020-06-09 03:58:12 +00:00
// Simulate IO.
simulateAustralianInternet()
2020-07-03 23:53:46 +00:00
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}
}
2020-06-08 22:19:13 +00:00
case NoopAction:
// do nothing.
case BestTrapAction:
return ch.EditMessage(messageID, "Astolfo.")
default:
return errors.New("Unknown action.")
}
return nil
}
func (ch *Channel) CompleteMessage(words []string, i int) (entries []cchat.CompletionEntry) {
2020-05-22 20:57:35 +00:00
switch {
case strings.HasPrefix("complete", words[i]):
entries = makeCompletion(
"complete",
"complete me",
"complete you",
"complete everyone",
)
case lookbackCheck(words, i, "complete", "me"):
entries = makeCompletion("me")
case lookbackCheck(words, i, "complete", "you"):
entries = makeCompletion("you")
case lookbackCheck(words, i, "complete", "everyone"):
entries = makeCompletion("everyone")
case lookbackCheck(words, i, "best", "trap:"):
entries = makeCompletion(
"trap: Astolfo",
"trap: Hackadoll No. 3",
"trap: Totsuka",
"trap: Felix Argyle",
)
2020-05-22 20:57:35 +00:00
default:
2020-07-01 20:37:56 +00:00
var found = map[string]struct{}{}
ch.messageMutex.Lock()
defer ch.messageMutex.Unlock()
// Look for members.
for _, id := range ch.messageids {
if msg := ch.messages[id]; strings.HasPrefix(msg.author.Content, words[i]) {
2020-07-01 20:37:56 +00:00
if _, ok := found[msg.author.Content]; ok {
continue
}
found[msg.author.Content] = struct{}{}
entries = append(entries, cchat.CompletionEntry{
2020-06-12 18:21:12 +00:00
Raw: msg.author.Content,
Text: msg.author,
IconURL: avatarURL,
})
}
}
2020-05-22 20:57:35 +00:00
}
return
}
func makeCompletion(word ...string) []cchat.CompletionEntry {
var entries = make([]cchat.CompletionEntry, len(word))
for i, w := range word {
2020-06-11 00:41:14 +00:00
entries[i].Raw = w
entries[i].Text.Content = w
entries[i].IconURL = avatarURL
}
return entries
}
// completion will only override `this'.
func lookbackCheck(words []string, i int, prev, this string) bool {
return strings.HasPrefix(this, words[i]) && i > 0 && words[i-1] == prev
2020-05-22 20:57:35 +00:00
}
2020-07-03 23:53:46 +00:00
// 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
}
2020-05-23 02:44:50 +00:00
func generateChannels(s *Session, amount int) []cchat.Server {
2020-05-20 07:13:12 +00:00
var channels = make([]cchat.Server, amount)
for i := range channels {
2020-05-20 21:23:44 +00:00
channels[i] = &Channel{
2020-06-04 04:36:46 +00:00
id: atomic.AddUint32(&s.lastid, 1),
name: "#" + randomdata.Noun(),
username: text.Rich{
Content: s.username,
// hot pink-ish colored
Segments: []text.Segment{segments.NewColored(s.username, 0xE88AF8)},
},
2020-05-20 21:23:44 +00:00
}
2020-05-20 07:13:12 +00:00
}
2020-06-09 03:58:12 +00:00
2020-05-20 07:13:12 +00:00
return channels
}
func randClamp(min, max int) int {
return rand.Intn(max-min) + min
}