mirror of
https://github.com/diamondburned/cchat-mock.git
synced 2024-10-18 05:24:30 +00:00
310 lines
7.4 KiB
Go
310 lines
7.4 KiB
Go
package channel
|
|
|
|
import (
|
|
"context"
|
|
"math/rand"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/diamondburned/cchat"
|
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
|
"github.com/diamondburned/cchat-mock/internal/message"
|
|
"github.com/diamondburned/cchat-mock/internal/typing"
|
|
"github.com/diamondburned/cchat/utils/empty"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// FetchBacklog is the number of messages to fake-fetmsgr.
|
|
const FetchBacklog = 35
|
|
const maxBacklog = FetchBacklog * 2
|
|
|
|
// max number to add to before the next author, with rand.Intn(limit) + incr.
|
|
const sameAuthorLimit = 6
|
|
|
|
type Messenger struct {
|
|
empty.Messenger
|
|
channel *Channel
|
|
|
|
send MessageSender
|
|
edit chan message.Message // id
|
|
del chan message.Header
|
|
typ typing.Subscriber
|
|
|
|
messageMutex sync.Mutex
|
|
messages map[uint32]message.Message
|
|
messageids []uint32 // indices
|
|
|
|
// 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
|
|
}
|
|
|
|
var _ cchat.Messenger = (*Messenger)(nil)
|
|
|
|
func NewMessenger(ch *Channel) *Messenger {
|
|
msgr := Messenger{}
|
|
msgr.channel = ch
|
|
// Initialize.
|
|
msgr.messages = make(map[uint32]message.Message, FetchBacklog)
|
|
msgr.messageids = make([]uint32, 0, FetchBacklog)
|
|
|
|
// Allocate 3 channels that we won't clean up, because we're lazy.
|
|
msgr.send = NewMessageSender(&msgr)
|
|
msgr.edit = make(chan message.Message)
|
|
msgr.del = make(chan message.Header)
|
|
msgr.typ = typing.NewSubscriber(message.NewAuthor(msgr.channel.user.Rich()))
|
|
|
|
return &msgr
|
|
}
|
|
|
|
func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
|
// Is this a fresh channel? If yes, generate messages with some IO latency.
|
|
if len(msgr.messageids) == 0 || msgr.messages == nil {
|
|
// Simulate IO and error.
|
|
if err := internet.SimulateAustralianCtx(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate the backlog.
|
|
for i := 0; i < FetchBacklog; i++ {
|
|
msgr.addMessage(msgr.randomMsg(), ct)
|
|
}
|
|
} else {
|
|
// Else, flush the old backlog over.
|
|
for i := range msgr.messages {
|
|
ct.CreateMessage(msgr.messages[i])
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
|
|
go func() {
|
|
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()
|
|
|
|
for {
|
|
select {
|
|
case msg := <-msgr.send.ch:
|
|
msgr.addMessage(message.Echo(
|
|
msg,
|
|
msgr.nextID(),
|
|
message.NewAuthor(msgr.channel.user.Rich()),
|
|
), ct)
|
|
|
|
case msg := <-msgr.edit:
|
|
ct.UpdateMessage(msg)
|
|
|
|
case msh := <-msgr.del:
|
|
msgr.deleteMessage(msh, ct)
|
|
|
|
case <-ticker.C:
|
|
msgr.addMessage(msgr.randomMsg(), ct)
|
|
|
|
case <-editTick.C:
|
|
var old = msgr.randomOldMsg()
|
|
msgr.updateMessage(message.NewRandomFromMessage(old), ct)
|
|
|
|
// case <-deleteTick.C:
|
|
// var old = msgr.randomOldMsg()
|
|
// msgr.deleteMessage(message.Header{old.id, time.Now()}, container)
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return stop, nil
|
|
}
|
|
|
|
func (msgr *Messenger) nextID() (id uint32) {
|
|
return atomic.AddUint32(&msgr.incrID, 1)
|
|
}
|
|
|
|
func (msgr *Messenger) AsEditor() cchat.Editor { return msgr }
|
|
|
|
// IsEditable returns true if the message belongs to the author.
|
|
func (msgr *Messenger) IsEditable(id string) bool {
|
|
i, err := message.ParseID(id)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
msgr.messageMutex.Lock()
|
|
defer msgr.messageMutex.Unlock()
|
|
|
|
m, ok := msgr.messages[i]
|
|
if ok {
|
|
// Editable if same author.
|
|
return m.Author().Name().String() == msgr.channel.user.String()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (msgr *Messenger) RawContent(id string) (string, error) {
|
|
i, err := message.ParseID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
msgr.messageMutex.Lock()
|
|
defer msgr.messageMutex.Unlock()
|
|
|
|
m, ok := msgr.messages[i]
|
|
if ok {
|
|
return m.Content().String(), nil
|
|
}
|
|
|
|
return "", errors.New("Message not found")
|
|
}
|
|
|
|
func (msgr *Messenger) Edit(id, content string) error {
|
|
i, err := message.ParseID(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := internet.SimulateAustralian(); err != nil {
|
|
return err
|
|
}
|
|
|
|
msgr.messageMutex.Lock()
|
|
defer msgr.messageMutex.Unlock()
|
|
|
|
m, ok := msgr.messages[i]
|
|
if ok {
|
|
m.SetContent(content)
|
|
msgr.messages[i] = m
|
|
msgr.edit <- m
|
|
|
|
return nil
|
|
}
|
|
|
|
return errors.New("Message not found.")
|
|
}
|
|
|
|
func (msgr *Messenger) addMessage(msg message.Message, container cchat.MessagesContainer) {
|
|
msgr.messageMutex.Lock()
|
|
|
|
// Clean up the backlog.
|
|
if clean := len(msgr.messages) - maxBacklog; clean > 0 {
|
|
// Remove them from the map.
|
|
for _, id := range msgr.messageids[:clean] {
|
|
delete(msgr.messages, id)
|
|
}
|
|
|
|
// Cut the message IDs away by shifting the slice.
|
|
msgr.messageids = append(msgr.messageids[:0], msgr.messageids[clean:]...)
|
|
}
|
|
|
|
msgr.messages[msg.RealID()] = msg
|
|
msgr.messageids = append(msgr.messageids, msg.RealID())
|
|
|
|
msgr.messageMutex.Unlock()
|
|
|
|
container.CreateMessage(msg)
|
|
}
|
|
|
|
func (msgr *Messenger) updateMessage(msg message.Message, container cchat.MessagesContainer) {
|
|
msgr.messageMutex.Lock()
|
|
|
|
_, ok := msgr.messages[msg.RealID()]
|
|
if ok {
|
|
msgr.messages[msg.RealID()] = msg
|
|
}
|
|
|
|
msgr.messageMutex.Unlock()
|
|
|
|
if ok {
|
|
container.UpdateMessage(msg)
|
|
}
|
|
}
|
|
|
|
func (msgr *Messenger) deleteMessage(msg message.Header, container cchat.MessagesContainer) {
|
|
msgr.messageMutex.Lock()
|
|
|
|
// Delete from the map.
|
|
delete(msgr.messages, msg.RealID())
|
|
|
|
// Delete from the ordered slice.
|
|
var ok bool
|
|
for i, id := range msgr.messageids {
|
|
if id == msg.RealID() {
|
|
msgr.messageids = append(msgr.messageids[:i], msgr.messageids[i+1:]...)
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
|
|
msgr.messageMutex.Unlock()
|
|
|
|
if ok {
|
|
container.DeleteMessage(msg)
|
|
}
|
|
}
|
|
|
|
// randomMsgID returns a random recent message ID.
|
|
func (msgr *Messenger) randomOldMsg() message.Message {
|
|
msgr.messageMutex.Lock()
|
|
defer msgr.messageMutex.Unlock()
|
|
|
|
// Pick a random index from last, clamped to 10 and len channel.
|
|
n := len(msgr.messageids) - 1 - rand.Intn(len(msgr.messageids))%10
|
|
return msgr.messages[msgr.messageids[n]]
|
|
}
|
|
|
|
// randomMsg uses top of the state algorithms to return fair and balanced
|
|
// messages suitable for rigorous testing.
|
|
func (msgr *Messenger) randomMsg() (msg message.Message) {
|
|
msgr.messageMutex.Lock()
|
|
defer msgr.messageMutex.Unlock()
|
|
|
|
// If we don't have any messages, then skip.
|
|
if len(msgr.messages) == 0 {
|
|
return message.Random(msgr.nextID())
|
|
}
|
|
|
|
// Add a random number into incrAuthor and determine if that should be
|
|
// enough to generate a new author.
|
|
msgr.incrAuthor += uint8(rand.Intn(sameAuthorLimit)) // 1~6 appearances
|
|
|
|
var lastID = msgr.messageids[len(msgr.messageids)-1]
|
|
var lastAu = msgr.messages[lastID].RealAuthor()
|
|
|
|
// 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 !lastAu.Equal(msg.RealAuthor()) && msgr.incrAuthor < sameAuthorLimit {
|
|
msg = message.RandomWithAuthor(msgr.nextID(), lastAu)
|
|
} else {
|
|
msg = message.Random(msgr.nextID())
|
|
msgr.incrAuthor = 0 // reset
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (msgr *Messenger) AsSender() cchat.Sender {
|
|
return msgr.send
|
|
}
|
|
|
|
func (msgr *Messenger) AsActioner() cchat.Actioner {
|
|
return &MessageActioner{msgr}
|
|
}
|
|
|
|
func (msgr *Messenger) AsTypingIndicator() cchat.TypingIndicator {
|
|
return msgr.typ
|
|
}
|