mirror of
https://github.com/diamondburned/cchat-mock.git
synced 2025-03-20 17:09:19 +00:00
Updated to mock v0.0.19
This commit is contained in:
parent
407aa1a13d
commit
cd71430167
255
channel.go
255
channel.go
|
@ -1,10 +1,10 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
|
@ -12,18 +12,35 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-mock/segments"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {}
|
||||
// FetchBacklog is the number of messages to fake-fetch.
|
||||
const FetchBacklog = 35
|
||||
|
||||
// max number to add to before the next author, with rand.Intn(limit) + incr.
|
||||
const sameAuthorLimit = 12
|
||||
|
||||
type Channel struct {
|
||||
id uint32
|
||||
name string
|
||||
username text.Rich
|
||||
|
||||
done chan struct{}
|
||||
send chan cchat.SendableMessage // ideally this should be another type
|
||||
lastID uint32
|
||||
send chan cchat.SendableMessage // ideally this should be another type
|
||||
edit chan Message // id
|
||||
|
||||
messageMutex sync.Mutex
|
||||
messageIDs map[string]int
|
||||
messages []Message
|
||||
|
||||
// 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
|
||||
|
||||
//
|
||||
busyWg sync.WaitGroup
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -32,54 +49,46 @@ var (
|
|||
_ cchat.ServerMessageSender = (*Channel)(nil)
|
||||
_ cchat.ServerMessageSendCompleter = (*Channel)(nil)
|
||||
_ cchat.ServerNickname = (*Channel)(nil)
|
||||
_ cchat.ServerMessageEditor = (*Channel)(nil)
|
||||
_ cchat.ServerMessageActioner = (*Channel)(nil)
|
||||
)
|
||||
|
||||
func (ch *Channel) ID() string {
|
||||
return strconv.Itoa(int(ch.id))
|
||||
}
|
||||
|
||||
func (ch *Channel) Name(labeler cchat.LabelContainer) error {
|
||||
func (ch *Channel) Name(labeler cchat.LabelContainer) (func(), error) {
|
||||
labeler.SetLabel(text.Rich{Content: ch.name})
|
||||
return nil
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (ch *Channel) Nickname(labeler cchat.LabelContainer) error {
|
||||
func (ch *Channel) Nickname(labeler cchat.LabelContainer) (func(), error) {
|
||||
labeler.SetLabel(ch.username)
|
||||
return nil
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (ch *Channel) JoinServer(container cchat.MessagesContainer) error {
|
||||
// Emulate IO.
|
||||
emulateAustralianInternet()
|
||||
func (ch *Channel) JoinServer(container cchat.MessagesContainer) (func(), error) {
|
||||
// Is this a fresh channel? If yes, generate messages with some IO latency.
|
||||
if ch.messageIDs == nil || len(ch.messages) == 0 {
|
||||
// Emulate IO.
|
||||
emulateAustralianInternet()
|
||||
|
||||
var lastAuthor text.Rich
|
||||
var lastCounter int
|
||||
// Initialize.
|
||||
ch.messageIDs = map[string]int{}
|
||||
ch.messages = make([]Message, 0, FetchBacklog)
|
||||
|
||||
var nextID = func() uint32 {
|
||||
id := ch.lastID
|
||||
ch.lastID++
|
||||
return id
|
||||
}
|
||||
var randomMsg = func() Message {
|
||||
// Try and reuse the author multiple times.
|
||||
if lastCounter++; lastCounter < randClamp(2, 5) {
|
||||
return randomMessageWithAuthor(nextID(), lastAuthor)
|
||||
// Allocate 2 channels that we won't clean up, because we're lazy.
|
||||
ch.send = make(chan cchat.SendableMessage)
|
||||
ch.edit = make(chan Message)
|
||||
|
||||
// Generate the backlog.
|
||||
for i := 0; i < FetchBacklog; i++ {
|
||||
ch.addMessage(randomMessage(ch.nextID()), container)
|
||||
}
|
||||
|
||||
msg := randomMessage(nextID())
|
||||
lastAuthor = msg.author
|
||||
lastCounter = 0
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// Write the backlog.
|
||||
for i := 0; i < 30; i++ {
|
||||
container.CreateMessage(randomMsg())
|
||||
}
|
||||
|
||||
ch.done = make(chan struct{})
|
||||
ch.send = make(chan cchat.SendableMessage)
|
||||
// Initialize channels for use.
|
||||
doneCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(4 * time.Second)
|
||||
|
@ -94,26 +103,141 @@ func (ch *Channel) JoinServer(container cchat.MessagesContainer) error {
|
|||
for {
|
||||
select {
|
||||
case msg := <-ch.send:
|
||||
container.CreateMessage(echoMessage(msg, nextID(), ch.username))
|
||||
ch.addMessage(echoMessage(msg, ch.nextID(), ch.username), container)
|
||||
|
||||
case msg := <-ch.edit:
|
||||
container.UpdateMessage(msg)
|
||||
|
||||
case <-ticker.C:
|
||||
container.CreateMessage(randomMsg())
|
||||
ch.addMessage(ch.randomMsg(), container)
|
||||
|
||||
case <-editTick.C:
|
||||
container.UpdateMessage(newRandomMessage(ch.lastID, lastAuthor))
|
||||
var old = ch.randomOldMsg()
|
||||
ch.updateMessage(newRandomMessage(old.id, old.author), container)
|
||||
|
||||
case <-deleteTick.C:
|
||||
container.DeleteMessage(newEmptyMessage(ch.lastID, lastAuthor))
|
||||
case <-ch.done:
|
||||
var old = ch.randomOldMsg()
|
||||
ch.deleteMessage(MessageHeader{old.id, time.Now()}, container)
|
||||
|
||||
case <-doneCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() { doneCh <- struct{}{} }, nil
|
||||
}
|
||||
|
||||
func (ch *Channel) RawMessageContent(id string) (string, error) {
|
||||
ch.messageMutex.Lock()
|
||||
defer ch.messageMutex.Unlock()
|
||||
|
||||
ix, ok := ch.messageIDs[id]
|
||||
if !ok {
|
||||
return "", errors.New("Message not found")
|
||||
}
|
||||
|
||||
return ch.messages[ix].content, nil
|
||||
}
|
||||
|
||||
func (ch *Channel) EditMessage(id, content string) error {
|
||||
emulateAustralianInternet()
|
||||
|
||||
ch.messageMutex.Lock()
|
||||
defer ch.messageMutex.Unlock()
|
||||
|
||||
ix, ok := ch.messageIDs[id]
|
||||
if !ok {
|
||||
return errors.New("ID not found")
|
||||
}
|
||||
|
||||
msg := ch.messages[ix]
|
||||
msg.content = content
|
||||
|
||||
ch.messages[ix] = msg
|
||||
ch.edit <- msg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *Channel) LeaveServer() error {
|
||||
ch.done <- struct{}{}
|
||||
ch.send = nil
|
||||
return nil
|
||||
func (ch *Channel) addMessage(msg Message, container cchat.MessagesContainer) {
|
||||
ch.messageMutex.Lock()
|
||||
defer ch.messageMutex.Unlock()
|
||||
|
||||
// Clean up the backlog.
|
||||
if len(ch.messages) > FetchBacklog*2 {
|
||||
|
||||
}
|
||||
ch.messages = append(ch.messages, msg)
|
||||
container.CreateMessage(msg)
|
||||
}
|
||||
|
||||
func (ch *Channel) updateMessage(msg Message, container cchat.MessagesContainer) {
|
||||
ch.messageMutex.Lock()
|
||||
defer ch.messageMutex.Unlock()
|
||||
|
||||
ix, ok := ch.messageIDs[msg.ID()]
|
||||
if !ok {
|
||||
// Unknown message.
|
||||
return
|
||||
}
|
||||
|
||||
ch.messages[ix] = msg
|
||||
container.UpdateMessage(msg)
|
||||
}
|
||||
|
||||
func (ch *Channel) deleteMessage(msg MessageHeader, container cchat.MessagesContainer) {
|
||||
ch.messageMutex.Lock()
|
||||
defer ch.messageMutex.Unlock()
|
||||
|
||||
ix, ok := ch.messageIDs[msg.ID()]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(ch.messageIDs, msg.ID())
|
||||
ch.messages = append(ch.messages[:ix], ch.messages[ix+1:]...)
|
||||
container.DeleteMessage(msg)
|
||||
}
|
||||
|
||||
// randomMsgID returns a random recent message ID.
|
||||
func (ch *Channel) randomOldMsg() Message {
|
||||
ch.messageMutex.Lock()
|
||||
defer ch.messageMutex.Unlock()
|
||||
|
||||
// Pick a random number, clamped to 10 and len channel.
|
||||
n := rand.Intn(len(ch.messages)) % 10
|
||||
return ch.messages[n]
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Should we generate a new author for the new message?
|
||||
if ch.incrAuthor > sameAuthorLimit {
|
||||
msg = randomMessage(ch.nextID())
|
||||
} else {
|
||||
last := ch.messages[len(ch.messages)-1]
|
||||
msg = randomMessageWithAuthor(ch.nextID(), last.author)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ch *Channel) nextID() (id uint32) {
|
||||
return atomic.AddUint32(&ch.incrID, 1)
|
||||
}
|
||||
|
||||
func (ch *Channel) SendMessage(msg cchat.SendableMessage) error {
|
||||
|
@ -131,6 +255,47 @@ func (ch *Channel) SendMessage(msg cchat.SendableMessage) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
DeleteAction = "Delete"
|
||||
NoopAction = "No-op"
|
||||
BestTrapAction = "Print best trap"
|
||||
)
|
||||
|
||||
func (ch *Channel) MessageActions() []string {
|
||||
return []string{
|
||||
DeleteAction,
|
||||
NoopAction,
|
||||
BestTrapAction,
|
||||
}
|
||||
}
|
||||
|
||||
// DoMessageAction will be blocked by IO. As goes for every other method that
|
||||
// takes a container: the frontend should call this in a goroutine.
|
||||
func (ch *Channel) DoMessageAction(c cchat.MessagesContainer, action, messageID string) error {
|
||||
switch action {
|
||||
case DeleteAction:
|
||||
i, err := strconv.Atoi(messageID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid ID")
|
||||
}
|
||||
|
||||
// Emulate IO.
|
||||
emulateAustralianInternet()
|
||||
ch.deleteMessage(MessageHeader{uint32(i), time.Now()}, c)
|
||||
|
||||
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) []string {
|
||||
switch {
|
||||
case strings.HasPrefix("complete", words[i]):
|
||||
|
|
3
go.mod
3
go.mod
|
@ -4,5 +4,6 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/Pallinder/go-randomdata v1.2.0
|
||||
github.com/diamondburned/cchat v0.0.15
|
||||
github.com/diamondburned/cchat v0.0.19
|
||||
github.com/pkg/errors v0.9.1
|
||||
)
|
||||
|
|
9
go.sum
9
go.sum
|
@ -30,6 +30,15 @@ github.com/diamondburned/cchat v0.0.14 h1:QpYRndVRBgg0DZHNrjbf+FNZ7dJlAsP7PlR+JA
|
|||
github.com/diamondburned/cchat v0.0.14/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.15 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
|
||||
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.16 h1:N+KxOhFJ+rpqs7mBYr40UWUyFeDA4js2W13KL8nuOOs=
|
||||
github.com/diamondburned/cchat v0.0.16/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.17 h1:n3x5p7hc4G+6osmjY1tsjbCd1+9YZ+vX/A6fcIm0HtE=
|
||||
github.com/diamondburned/cchat v0.0.17/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.18 h1:1nTPcYEumpLCangEV/oblNkZrZG9dQ432Ov1qvlzSNw=
|
||||
github.com/diamondburned/cchat v0.0.18/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.19 h1:XPZDqOR8P1tzTWVkYzRb6yZ1H6yU8tSUReGjklIqarw=
|
||||
github.com/diamondburned/cchat v0.0.19/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
53
message.go
53
message.go
|
@ -13,9 +13,23 @@ import (
|
|||
|
||||
const avatarURL = "https://gist.github.com/diamondburned/945744c2b5ce0aa0581c9267a4e5cf24/raw/598069da673093aaca4cd4aa0ede1a0e324e9a3a/astolfo_selfie.png"
|
||||
|
||||
type MessageHeader struct {
|
||||
id uint32
|
||||
time time.Time
|
||||
}
|
||||
|
||||
var _ cchat.MessageHeader = (*Message)(nil)
|
||||
|
||||
func (m MessageHeader) ID() string {
|
||||
return strconv.Itoa(int(m.id))
|
||||
}
|
||||
|
||||
func (m MessageHeader) Time() time.Time {
|
||||
return m.time
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
id uint32
|
||||
time time.Time
|
||||
MessageHeader
|
||||
author text.Rich
|
||||
content string
|
||||
nonce string
|
||||
|
@ -31,26 +45,24 @@ var (
|
|||
|
||||
func newEmptyMessage(id uint32, author text.Rich) Message {
|
||||
return Message{
|
||||
id: id,
|
||||
author: author,
|
||||
MessageHeader: MessageHeader{id: id},
|
||||
author: author,
|
||||
}
|
||||
}
|
||||
|
||||
func newRandomMessage(id uint32, author text.Rich) Message {
|
||||
return Message{
|
||||
id: id,
|
||||
time: time.Now(),
|
||||
author: author,
|
||||
content: randomdata.Paragraph(),
|
||||
MessageHeader: MessageHeader{id: id, time: time.Now()},
|
||||
author: author,
|
||||
content: randomdata.Paragraph(),
|
||||
}
|
||||
}
|
||||
|
||||
func echoMessage(sendable cchat.SendableMessage, id uint32, author text.Rich) Message {
|
||||
var echo = Message{
|
||||
id: id,
|
||||
time: time.Now(),
|
||||
author: author,
|
||||
content: sendable.Content(),
|
||||
MessageHeader: MessageHeader{id: id, time: time.Now()},
|
||||
author: author,
|
||||
content: sendable.Content(),
|
||||
}
|
||||
if noncer, ok := sendable.(cchat.MessageNonce); ok {
|
||||
echo.nonce = noncer.Nonce()
|
||||
|
@ -68,24 +80,13 @@ func randomMessage(id uint32) Message {
|
|||
}
|
||||
|
||||
func randomMessageWithAuthor(id uint32, author text.Rich) Message {
|
||||
var now = time.Now()
|
||||
|
||||
return Message{
|
||||
id: id,
|
||||
time: now,
|
||||
author: author,
|
||||
content: randomdata.Paragraph(),
|
||||
MessageHeader: MessageHeader{id: id, time: time.Now()},
|
||||
author: author,
|
||||
content: randomdata.Paragraph(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m Message) ID() string {
|
||||
return strconv.Itoa(int(m.id))
|
||||
}
|
||||
|
||||
func (m Message) Time() time.Time {
|
||||
return m.time
|
||||
}
|
||||
|
||||
func (m Message) Author() cchat.MessageAuthor {
|
||||
return Author{name: m.author}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ func (sv *Server) ID() string {
|
|||
return strconv.Itoa(int(sv.id))
|
||||
}
|
||||
|
||||
func (sv *Server) Name(labeler cchat.LabelContainer) error {
|
||||
func (sv *Server) Name(labeler cchat.LabelContainer) (func(), error) {
|
||||
labeler.SetLabel(text.Rich{Content: sv.name})
|
||||
return nil
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (sv *Server) Servers(container cchat.ServersContainer) error {
|
||||
|
|
|
@ -139,9 +139,9 @@ func (s *Session) ID() string {
|
|||
return s.username
|
||||
}
|
||||
|
||||
func (s *Session) Name(labeler cchat.LabelContainer) error {
|
||||
func (s *Session) Name(labeler cchat.LabelContainer) (func(), error) {
|
||||
labeler.SetLabel(text.Rich{Content: s.username})
|
||||
return nil
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (s *Session) Servers(container cchat.ServersContainer) error {
|
||||
|
@ -149,9 +149,9 @@ func (s *Session) Servers(container cchat.ServersContainer) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Icon(iconer cchat.IconContainer) error {
|
||||
func (s *Session) Icon(iconer cchat.IconContainer) (func(), error) {
|
||||
iconer.SetIcon(avatarURL)
|
||||
return nil
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (s *Session) Save() (map[string]string, error) {
|
||||
|
|
Loading…
Reference in a new issue