Migrated to cchat v2
This commit is contained in:
parent
0f83829667
commit
b841407af3
515
channel.go
515
channel.go
|
@ -1,515 +0,0 @@
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Pallinder/go-randomdata"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-mock/segments"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FetchBacklog is the number of messages to fake-fetch.
|
|
||||||
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 Channel struct {
|
|
||||||
id uint32
|
|
||||||
name string
|
|
||||||
username text.Rich
|
|
||||||
|
|
||||||
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
|
|
||||||
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.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 {
|
|
||||||
return strconv.Itoa(int(ch.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) Name() text.Rich {
|
|
||||||
return text.Rich{Content: ch.name}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// The given context is cancelled.
|
|
||||||
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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
labeler.SetLabel(ch.username)
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
|
||||||
// Is this a fresh channel? If yes, generate messages with some IO latency.
|
|
||||||
if len(ch.messageids) == 0 || ch.messages == nil {
|
|
||||||
// Simulate IO and error.
|
|
||||||
if err := simulateAustralianInternetCtx(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize.
|
|
||||||
ch.messages = make(map[uint32]Message, FetchBacklog)
|
|
||||||
ch.messageids = make([]uint32, 0, FetchBacklog)
|
|
||||||
|
|
||||||
// Allocate 3 channels that we won't clean up, because we're lazy.
|
|
||||||
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++ {
|
|
||||||
ch.addMessage(ch.randomMsg(), ct)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Else, flush the old backlog over.
|
|
||||||
for i := range ch.messages {
|
|
||||||
ct.CreateMessage(ch.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 := <-ch.send:
|
|
||||||
ch.addMessage(echoMessage(msg, ch.nextID(), newAuthor(ch.username)), ct)
|
|
||||||
|
|
||||||
case msg := <-ch.edit:
|
|
||||||
ct.UpdateMessage(msg)
|
|
||||||
|
|
||||||
case msh := <-ch.del:
|
|
||||||
ch.deleteMessage(msh, ct)
|
|
||||||
|
|
||||||
case <-ticker.C:
|
|
||||||
ch.addMessage(ch.randomMsg(), ct)
|
|
||||||
|
|
||||||
case <-editTick.C:
|
|
||||||
var old = ch.randomOldMsg()
|
|
||||||
ch.updateMessage(newRandomMessage(old.id, old.author), ct)
|
|
||||||
|
|
||||||
// case <-deleteTick.C:
|
|
||||||
// var old = ch.randomOldMsg()
|
|
||||||
// ch.deleteMessage(MessageHeader{old.id, time.Now()}, container)
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return stop, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.name.Content == ch.username.Content
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) RawMessageContent(id string) (string, error) {
|
|
||||||
i, err := parseID(id)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
ch.messageMutex.Lock()
|
|
||||||
defer ch.messageMutex.Unlock()
|
|
||||||
|
|
||||||
m, ok := ch.messages[i]
|
|
||||||
if ok {
|
|
||||||
return m.content, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("Message not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) EditMessage(id, content string) error {
|
|
||||||
i, err := parseID(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateAustralianInternet()
|
|
||||||
|
|
||||||
ch.messageMutex.Lock()
|
|
||||||
defer ch.messageMutex.Unlock()
|
|
||||||
|
|
||||||
m, ok := ch.messages[i]
|
|
||||||
if ok {
|
|
||||||
m.content = content
|
|
||||||
ch.messages[i] = m
|
|
||||||
ch.edit <- m
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("Message not found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) addMessage(msg Message, container cchat.MessagesContainer) {
|
|
||||||
ch.messageMutex.Lock()
|
|
||||||
|
|
||||||
// Clean up the backlog.
|
|
||||||
if clean := len(ch.messages) - maxBacklog; clean > 0 {
|
|
||||||
// Remove them from the map.
|
|
||||||
for _, id := range ch.messageids[:clean] {
|
|
||||||
delete(ch.messages, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut the message IDs away by shifting the slice.
|
|
||||||
ch.messageids = append(ch.messageids[:0], ch.messageids[clean:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
ch.messages[msg.id] = msg
|
|
||||||
ch.messageids = append(ch.messageids, msg.id)
|
|
||||||
|
|
||||||
ch.messageMutex.Unlock()
|
|
||||||
|
|
||||||
container.CreateMessage(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) updateMessage(msg Message, container cchat.MessagesContainer) {
|
|
||||||
ch.messageMutex.Lock()
|
|
||||||
|
|
||||||
_, ok := ch.messages[msg.id]
|
|
||||||
if ok {
|
|
||||||
ch.messages[msg.id] = msg
|
|
||||||
}
|
|
||||||
|
|
||||||
ch.messageMutex.Unlock()
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
container.UpdateMessage(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ch.messageMutex.Unlock()
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
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 index from last, clamped to 10 and len channel.
|
|
||||||
n := len(ch.messageids) - 1 - rand.Intn(len(ch.messageids))%10
|
|
||||||
return ch.messages[ch.messageids[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(sameAuthorLimit)) // 1~6 appearances
|
|
||||||
|
|
||||||
var lastID = ch.messageids[len(ch.messageids)-1]
|
|
||||||
var lastAu = ch.messages[lastID].author
|
|
||||||
|
|
||||||
// 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.name.Content != ch.username.Content && ch.incrAuthor < sameAuthorLimit {
|
|
||||||
msg = randomMessageWithAuthor(ch.nextID(), lastAu)
|
|
||||||
} else {
|
|
||||||
msg = randomMessage(ch.nextID())
|
|
||||||
ch.incrAuthor = 0 // reset
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) nextID() (id uint32) {
|
|
||||||
return atomic.AddUint32(&ch.incrID, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) SendMessage(msg cchat.SendableMessage) error {
|
|
||||||
if err := simulateAustralianInternet(); err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to send message")
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// Make no guarantee that a message may arrive immediately when the
|
|
||||||
// function exits.
|
|
||||||
<-time.After(time.Second)
|
|
||||||
ch.send <- msg
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
DeleteAction = "Delete"
|
|
||||||
NoopAction = "No-op"
|
|
||||||
BestTrapAction = "Who's the best trap?"
|
|
||||||
TriggerTypingAction = "Trigger Typing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ch *Channel) MessageActions(id string) []string {
|
|
||||||
return []string{
|
|
||||||
DeleteAction,
|
|
||||||
NoopAction,
|
|
||||||
BestTrapAction,
|
|
||||||
TriggerTypingAction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(action, messageID string) error {
|
|
||||||
switch action {
|
|
||||||
case DeleteAction, TriggerTypingAction:
|
|
||||||
i, err := strconv.Atoi(messageID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Invalid ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate IO.
|
|
||||||
simulateAustralianInternet()
|
|
||||||
|
|
||||||
switch action {
|
|
||||||
case DeleteAction:
|
|
||||||
ch.del <- MessageHeader{uint32(i), time.Now()}
|
|
||||||
case TriggerTypingAction:
|
|
||||||
ch.typ <- ch.messages[uint32(i)].author
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
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.name.Content, words[i]) {
|
|
||||||
if _, ok := found[msg.author.name.Content]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
found[msg.author.name.Content] = struct{}{}
|
|
||||||
|
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
|
||||||
Raw: msg.author.name.Content,
|
|
||||||
Text: msg.author.name,
|
|
||||||
IconURL: avatarURL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCompletion(word ...string) []cchat.CompletionEntry {
|
|
||||||
var entries = make([]cchat.CompletionEntry, len(word))
|
|
||||||
for i, w := range word {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
channels[i] = &Channel{
|
|
||||||
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)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
func randClamp(min, max int) int {
|
|
||||||
return rand.Intn(max-min) + min
|
|
||||||
}
|
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.14
|
||||||
require (
|
require (
|
||||||
github.com/Pallinder/go-randomdata v1.2.0
|
github.com/Pallinder/go-randomdata v1.2.0
|
||||||
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb
|
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb
|
||||||
github.com/diamondburned/cchat v0.0.46
|
github.com/diamondburned/cchat v0.2.11
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3
|
github.com/lucasb-eyer/go-colorful v1.0.3
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
golang.org/x/text v0.3.3 // indirect
|
golang.org/x/text v0.3.3 // indirect
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -1,13 +1,11 @@
|
||||||
github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=
|
github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=
|
||||||
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
|
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
|
||||||
|
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||||
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSkYxdRRzM8QUAuCswfLmaiBTd2UIU+54=
|
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSkYxdRRzM8QUAuCswfLmaiBTd2UIU+54=
|
||||||
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs=
|
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs=
|
||||||
github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I=
|
github.com/diamondburned/cchat v0.2.11 h1:w4c/6t02htGtVj6yIjznecOGMlkcj0TmmLy+K48gHeM=
|
||||||
github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.2.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
github.com/diamondburned/cchat v0.0.43 h1:HetAujSaUSdnQgAUZgprNLARjf/MSWXpCfWdvX2wOCU=
|
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||||
github.com/diamondburned/cchat v0.0.43/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
|
||||||
github.com/diamondburned/cchat v0.0.46 h1:fzm2XA9uGasX0uaic1AFfUMGA53PlO+GGmkYbx49A5k=
|
|
||||||
github.com/diamondburned/cchat v0.0.46/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||||
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
|
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Pallinder/go-randomdata"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/shared"
|
||||||
|
"github.com/diamondburned/cchat-mock/segments"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
empty.Server
|
||||||
|
id uint32
|
||||||
|
name string
|
||||||
|
user Username
|
||||||
|
|
||||||
|
messenger *Messenger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Server = (*Channel)(nil)
|
||||||
|
|
||||||
|
func NewChannels(state *shared.State, n int) []*Channel {
|
||||||
|
var channels = make([]*Channel, n)
|
||||||
|
for i := range channels {
|
||||||
|
channels[i] = NewChannel(state)
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
func AsCChatServers(channels []*Channel) []cchat.Server {
|
||||||
|
var cchs = make([]cchat.Server, len(channels))
|
||||||
|
for i, ch := range channels {
|
||||||
|
cchs[i] = ch
|
||||||
|
}
|
||||||
|
return cchs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChannel creates a new random channel.
|
||||||
|
func NewChannel(state *shared.State) *Channel {
|
||||||
|
return &Channel{
|
||||||
|
id: state.NextID(),
|
||||||
|
name: "#" + randomdata.Noun(),
|
||||||
|
user: Username{
|
||||||
|
Content: state.Username,
|
||||||
|
Segments: []text.Segment{
|
||||||
|
// hot pink-ish colored
|
||||||
|
segments.NewColoredSegment(state.Username, 0xE88AF8),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) ID() string {
|
||||||
|
return strconv.Itoa(int(ch.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) Name() text.Rich {
|
||||||
|
return text.Plain(ch.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) AsNicknamer() cchat.Nicknamer {
|
||||||
|
return ch.user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) AsMessenger() cchat.Messenger {
|
||||||
|
return ch.messenger
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/message"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageActioner struct {
|
||||||
|
msgr *Messenger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Actioner = (*MessageActioner)(nil)
|
||||||
|
|
||||||
|
func NewMessageActioner(msgr *Messenger) MessageActioner {
|
||||||
|
return MessageActioner{
|
||||||
|
msgr: msgr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeleteAction = "Delete"
|
||||||
|
NoopAction = "No-op"
|
||||||
|
BestTrapAction = "Who's the best trap?"
|
||||||
|
TriggerTypingAction = "Trigger Typing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (msga MessageActioner) Actions(id string) []string {
|
||||||
|
return []string{
|
||||||
|
DeleteAction,
|
||||||
|
NoopAction,
|
||||||
|
BestTrapAction,
|
||||||
|
TriggerTypingAction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoAction will be blocked by IO. As goes for every other method that
|
||||||
|
// takes a container: the frontend should call this in a goroutine.
|
||||||
|
func (msga MessageActioner) DoAction(action, messageID string) error {
|
||||||
|
switch action {
|
||||||
|
case DeleteAction, TriggerTypingAction:
|
||||||
|
i, err := strconv.Atoi(messageID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Invalid ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate IO.
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case DeleteAction:
|
||||||
|
msga.msgr.del <- message.NewHeader(uint32(i), time.Now())
|
||||||
|
case TriggerTypingAction:
|
||||||
|
msga.msgr.typ.TriggerTyping(msga.msgr.messages[uint32(i)].RealAuthor())
|
||||||
|
}
|
||||||
|
|
||||||
|
case NoopAction:
|
||||||
|
// do nothing.
|
||||||
|
|
||||||
|
case BestTrapAction:
|
||||||
|
return msga.msgr.EditMessage(messageID, "Astolfo.")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.New("Unknown action.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageCompleter struct {
|
||||||
|
msgr *Messenger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgc MessageCompleter) Complete(words []string, i int64) []cchat.CompletionEntry {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix("complete", words[i]):
|
||||||
|
return makeCompletion(
|
||||||
|
"complete",
|
||||||
|
"complete me",
|
||||||
|
"complete you",
|
||||||
|
"complete everyone",
|
||||||
|
)
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "complete", "me"):
|
||||||
|
return makeCompletion("me")
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "complete", "you"):
|
||||||
|
return makeCompletion("you")
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "complete", "everyone"):
|
||||||
|
return makeCompletion("everyone")
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "best", "femboys:"):
|
||||||
|
return makeCompletion(
|
||||||
|
"trap: Astolfo",
|
||||||
|
"trap: Hackadoll No. 3",
|
||||||
|
"trap: Totsuka",
|
||||||
|
"trap: Felix Argyle",
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
var found = map[string]struct{}{}
|
||||||
|
|
||||||
|
msgc.msgr.messageMutex.Lock()
|
||||||
|
defer msgc.msgr.messageMutex.Unlock()
|
||||||
|
|
||||||
|
var entries []cchat.CompletionEntry
|
||||||
|
|
||||||
|
// Look for members.
|
||||||
|
for _, id := range msgc.msgr.messageids {
|
||||||
|
if msg := msgc.msgr.messages[id]; strings.HasPrefix(msg.AuthorName(), words[i]) {
|
||||||
|
if _, ok := found[msg.AuthorName()]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
found[msg.AuthorName()] = struct{}{}
|
||||||
|
|
||||||
|
entries = append(entries, cchat.CompletionEntry{
|
||||||
|
Raw: msg.AuthorName(),
|
||||||
|
Text: msg.Author().Name(),
|
||||||
|
IconURL: msg.Author().Avatar(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCompletion(word ...string) []cchat.CompletionEntry {
|
||||||
|
var entries = make([]cchat.CompletionEntry, len(word))
|
||||||
|
for i, w := range word {
|
||||||
|
entries[i].Raw = w
|
||||||
|
entries[i].Text.Content = w
|
||||||
|
entries[i].IconURL = message.AvatarURL
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// completion will only override `this'.
|
||||||
|
func lookbackCheck(words []string, i int64, prev, this string) bool {
|
||||||
|
return strings.HasPrefix(this, words[i]) && i > 0 && words[i-1] == prev
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageSender struct {
|
||||||
|
msgr *Messenger
|
||||||
|
ch chan cchat.SendableMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Sender = (*MessageSender)(nil)
|
||||||
|
|
||||||
|
func NewMessageSender(msgr *Messenger) MessageSender {
|
||||||
|
return MessageSender{
|
||||||
|
msgr: msgr,
|
||||||
|
ch: make(chan cchat.SendableMessage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAttach returns false.
|
||||||
|
func (msgs MessageSender) CanAttach() bool { return false }
|
||||||
|
|
||||||
|
func (msgs MessageSender) Send(msg cchat.SendableMessage) error {
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to send message")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Make no guarantee that a message may arrive immediately when the
|
||||||
|
// function exits.
|
||||||
|
<-time.After(time.Second)
|
||||||
|
msgs.ch <- msg
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgs MessageSender) AsCompleter() cchat.Completer {
|
||||||
|
return &MessageCompleter{msgr: msgs.msgr}
|
||||||
|
}
|
|
@ -0,0 +1,303 @@
|
||||||
|
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 (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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()))
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
|
||||||
|
// MessageEditable returns true if the message belongs to the author.
|
||||||
|
func (msgr *Messenger) MessageEditable(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) RawMessageContent(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) EditMessage(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
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Username is the type for a username/nickname.
|
||||||
|
type Username text.Rich
|
||||||
|
|
||||||
|
func (u Username) String() string {
|
||||||
|
return text.Rich(u).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Username) Rich() text.Rich {
|
||||||
|
return text.Rich(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (u Username) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) {
|
||||||
|
if err := internet.SimulateAustralianCtx(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
labeler.SetLabel(text.Rich(u))
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package internet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// channel.go @ simulateAustralianInternet
|
||||||
|
CanFail = true
|
||||||
|
// 500ms ~ 3s
|
||||||
|
MinLatency = 500
|
||||||
|
MaxLatency = 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrTimedOut is returned when the simulated IO decides to fail.
|
||||||
|
var ErrTimedOut = errors.New("Australian Internet unsupported.")
|
||||||
|
|
||||||
|
// SimulateAustralian simulates network latency with errors.
|
||||||
|
func SimulateAustralian() error {
|
||||||
|
return SimulateAustralianCtx(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimulateAustralianCtx simulates network latency with errors.
|
||||||
|
func SimulateAustralianCtx(ctx context.Context) (err error) {
|
||||||
|
var ms = randClamp(MinLatency, MaxLatency)
|
||||||
|
|
||||||
|
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 CanFail && rand.Intn(100) < 20 {
|
||||||
|
return ErrTimedOut
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func randClamp(min, max int) int {
|
||||||
|
return rand.Intn(max-min) + min
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/aqs"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/segments"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AvatarURL = "" +
|
||||||
|
"https://gist.github.com/diamondburned/" +
|
||||||
|
"945744c2b5ce0aa0581c9267a4e5cf24/raw/" +
|
||||||
|
"598069da673093aaca4cd4aa0ede1a0e324e9a3a/" +
|
||||||
|
"astolfo_selfie.png"
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
name text.Rich
|
||||||
|
char aqs.Character
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Author = (*Author)(nil)
|
||||||
|
|
||||||
|
func NewAuthor(name text.Rich) Author {
|
||||||
|
return Author{name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomAuthor() Author {
|
||||||
|
var char = aqs.RandomCharacter()
|
||||||
|
return Author{
|
||||||
|
char: char,
|
||||||
|
name: text.Rich{
|
||||||
|
Content: char.Name,
|
||||||
|
Segments: []text.Segment{
|
||||||
|
segments.NewColorfulSegment(char.Name, char.NameColor()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) ID() string {
|
||||||
|
return a.name.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) Name() text.Rich {
|
||||||
|
return a.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) Avatar() string {
|
||||||
|
if a.char.ImageURL != "" {
|
||||||
|
return a.char.ImageURL
|
||||||
|
}
|
||||||
|
return AvatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if this author is the same as the given other author.
|
||||||
|
func (a Author) Equal(other Author) bool {
|
||||||
|
return a.name.Content == other.name.Content
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Header struct {
|
||||||
|
id uint32
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.MessageHeader = (*Message)(nil)
|
||||||
|
|
||||||
|
func ParseID(id string) (uint32, error) {
|
||||||
|
i, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint32(i), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHeader(id uint32, t time.Time) Header {
|
||||||
|
return Header{
|
||||||
|
id: id,
|
||||||
|
time: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Header) ID() string {
|
||||||
|
return strconv.Itoa(int(m.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Header) RealID() uint32 {
|
||||||
|
return m.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Header) Time() time.Time {
|
||||||
|
return m.time
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/aqs/incr"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
|
||||||
|
_ "github.com/diamondburned/aqs/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Header
|
||||||
|
author Author
|
||||||
|
content string
|
||||||
|
nonce string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.MessageCreate = (*Message)(nil)
|
||||||
|
_ cchat.MessageUpdate = (*Message)(nil)
|
||||||
|
_ cchat.MessageDelete = (*Message)(nil)
|
||||||
|
_ cchat.Noncer = (*Message)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewEmpty(id uint32, author Author) Message {
|
||||||
|
return Message{
|
||||||
|
Header: Header{id: id},
|
||||||
|
author: author,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRandomFromMessage(old Message) Message {
|
||||||
|
return NewRandom(old.id, old.author)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRandom(id uint32, author Author) Message {
|
||||||
|
return Message{
|
||||||
|
Header: Header{id: id, time: time.Now()},
|
||||||
|
author: author,
|
||||||
|
content: incr.RandomQuote(author.char),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Echo(sendable cchat.SendableMessage, id uint32, author Author) Message {
|
||||||
|
var echo = Message{
|
||||||
|
Header: Header{id: id, time: time.Now()},
|
||||||
|
author: author,
|
||||||
|
content: sendable.Content(),
|
||||||
|
}
|
||||||
|
if noncer := sendable.AsNoncer(); noncer != nil {
|
||||||
|
echo.nonce = noncer.Nonce()
|
||||||
|
}
|
||||||
|
return echo
|
||||||
|
}
|
||||||
|
|
||||||
|
func Random(id uint32) Message {
|
||||||
|
return RandomWithAuthor(id, RandomAuthor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomWithAuthor(id uint32, author Author) Message {
|
||||||
|
return Message{
|
||||||
|
Header: Header{id: id, time: time.Now()},
|
||||||
|
author: author,
|
||||||
|
content: incr.RandomQuote(author.char),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) Author() cchat.Author {
|
||||||
|
return m.author
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) RealAuthor() Author {
|
||||||
|
return m.author
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorName returns the message author's username in string.
|
||||||
|
func (m Message) AuthorName() string {
|
||||||
|
return m.author.name.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) Content() text.Rich {
|
||||||
|
return text.Rich{Content: m.content}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) Nonce() string {
|
||||||
|
return m.nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mentioned is true when the message content contains the author's name.
|
||||||
|
func (m Message) Mentioned() bool {
|
||||||
|
// hack
|
||||||
|
return strings.Contains(m.content, m.author.name.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) SetContent(content string) {
|
||||||
|
m.content = content
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Pallinder/go-randomdata"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/channel"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/shared"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
empty.Server
|
||||||
|
state *shared.State
|
||||||
|
id uint32
|
||||||
|
name string
|
||||||
|
children ChannelList
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Server = (*Server)(nil)
|
||||||
|
|
||||||
|
func NewServers(state *shared.State, n int) []*Server {
|
||||||
|
var servers = make([]*Server, n)
|
||||||
|
for i := range servers {
|
||||||
|
servers[i] = New(state)
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsCChatServers casts a list of *Server to a list of cchat.Server.
|
||||||
|
func AsCChatServers(servers []*Server) []cchat.Server {
|
||||||
|
var csvs = make([]cchat.Server, len(servers))
|
||||||
|
for i, sv := range servers {
|
||||||
|
csvs[i] = sv
|
||||||
|
}
|
||||||
|
return csvs
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(state *shared.State) *Server {
|
||||||
|
return &Server{
|
||||||
|
state: state,
|
||||||
|
id: state.NextID(),
|
||||||
|
name: randomdata.Noun(),
|
||||||
|
children: RandomChannels(state, rand.Intn(12)+5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *Server) ID() string {
|
||||||
|
return strconv.Itoa(int(sv.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *Server) Name() text.Rich {
|
||||||
|
return text.Plain(sv.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *Server) AsLister() cchat.Lister {
|
||||||
|
return sv.children
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelList []cchat.Server
|
||||||
|
|
||||||
|
func RandomChannels(state *shared.State, n int) ChannelList {
|
||||||
|
return channel.AsCChatServers(channel.NewChannels(state, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chl ChannelList) Servers(container cchat.ServersContainer) error {
|
||||||
|
// IO time.
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers([]cchat.Server(chl))
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/session"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authenticator struct{}
|
||||||
|
|
||||||
|
var _ cchat.Authenticator = (*Authenticator)(nil)
|
||||||
|
|
||||||
|
func (Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
|
||||||
|
return []cchat.AuthenticateEntry{
|
||||||
|
{
|
||||||
|
Name: "Username",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Password (ignored)",
|
||||||
|
Secret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Paragraph (ignored)",
|
||||||
|
Multiline: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Authenticator) Authenticate(form []string) (cchat.Session, error) {
|
||||||
|
// SLOW IO TIME.
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Authentication failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.New(form[0], ""), nil
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Configurator struct{}
|
||||||
|
|
||||||
|
func (Configurator) Configuration() (map[string]string, error) {
|
||||||
|
return map[string]string{
|
||||||
|
// refer to internet.go
|
||||||
|
"internet.CanFail": strconv.FormatBool(internet.CanFail),
|
||||||
|
"internet.MinLatency": strconv.Itoa(internet.MinLatency),
|
||||||
|
"internet.MaxLatency": strconv.Itoa(internet.MaxLatency),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Configurator) SetConfiguration(config map[string]string) error {
|
||||||
|
for _, err := range []error{
|
||||||
|
// shit code, would not recommend. It's only an ok-ish idea here because
|
||||||
|
// unmarshalConfig() returns ErrInvalidConfigAtField.
|
||||||
|
unmarshalConfig(config, "internet.CanFail", &internet.CanFail),
|
||||||
|
unmarshalConfig(config, "internet.MinLatency", &internet.MinLatency),
|
||||||
|
unmarshalConfig(config, "internet.MaxLatency", &internet.MaxLatency),
|
||||||
|
} {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalConfig(config map[string]string, key string, value interface{}) error {
|
||||||
|
if err := json.Unmarshal([]byte(config[key]), value); err != nil {
|
||||||
|
return &cchat.ErrInvalidConfigAtField{
|
||||||
|
Key: key,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/session"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/shared"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
empty.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Service = (*Service)(nil)
|
||||||
|
|
||||||
|
func (s Service) Name() text.Rich {
|
||||||
|
return text.Rich{Content: "Mock"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) RestoreSession(storage map[string]string) (cchat.Session, error) {
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Restore failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := shared.RestoreState(storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.FromState(state), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) Authenticate() cchat.Authenticator {
|
||||||
|
return Authenticator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) AsConfigurator() cchat.Configurator {
|
||||||
|
return Configurator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) AsSessionRestorer() cchat.SessionRestorer {
|
||||||
|
return s
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Pallinder/go-randomdata"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Commander struct{}
|
||||||
|
|
||||||
|
func (c *Commander) RunCommand(cmds []string, w io.Writer) error {
|
||||||
|
switch cmd := arg(cmds, 0); cmd {
|
||||||
|
case "ls":
|
||||||
|
fmt.Fprintln(w, "Commands: ls, random")
|
||||||
|
|
||||||
|
case "random":
|
||||||
|
// callback used to generate stuff and stream into readcloser
|
||||||
|
var generator func() string
|
||||||
|
// number of times to generate the word
|
||||||
|
var times = 1
|
||||||
|
|
||||||
|
switch arg(cmds, 1) {
|
||||||
|
case "paragraph":
|
||||||
|
generator = randomdata.Paragraph
|
||||||
|
case "noun":
|
||||||
|
generator = randomdata.Noun
|
||||||
|
case "silly_name":
|
||||||
|
generator = randomdata.SillyName
|
||||||
|
default:
|
||||||
|
return errors.New("Usage: random <paragraph|noun|silly_name> [repeat]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := arg(cmds, 2); n != "" {
|
||||||
|
i, err := strconv.Atoi(n)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to parse repeat number")
|
||||||
|
}
|
||||||
|
times = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < times; i++ {
|
||||||
|
// Yes, we're simulating this even in something as trivial as a
|
||||||
|
// command prompt.
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
fmt.Fprintln(w, "Error:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(w, generator())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown command: %s", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Commander) AsCompleter() cchat.Completer { return s }
|
||||||
|
|
||||||
|
func (s *Commander) Complete(words []string, i int64) []cchat.CompletionEntry {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix("ls", words[i]):
|
||||||
|
return newCompEntries("ls")
|
||||||
|
|
||||||
|
case strings.HasPrefix("random", words[i]):
|
||||||
|
return newCompEntries(
|
||||||
|
"random paragraph",
|
||||||
|
"random noun",
|
||||||
|
"random silly_name",
|
||||||
|
)
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "random", "paragraph"):
|
||||||
|
return newCompEntries("paragraph")
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "random", "noun"):
|
||||||
|
return newCompEntries("noun")
|
||||||
|
|
||||||
|
case lookbackCheck(words, i, "random", "silly_name"):
|
||||||
|
return newCompEntries("silly_name")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// completion will only override `this'.
|
||||||
|
func lookbackCheck(words []string, i int64, prev, this string) bool {
|
||||||
|
return strings.HasPrefix(this, words[i]) && i > 0 && words[i-1] == prev
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCompEntries(raws ...string) []cchat.CompletionEntry {
|
||||||
|
var entries = make([]cchat.CompletionEntry, len(raws))
|
||||||
|
for i, raw := range raws {
|
||||||
|
entries[i] = cchat.CompletionEntry{
|
||||||
|
Raw: raw,
|
||||||
|
Text: text.Plain(raw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func arg(sl []string, i int) string {
|
||||||
|
if i >= len(sl) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sl[i]
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/message"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/server"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/shared"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
empty.Session
|
||||||
|
State *shared.State
|
||||||
|
ServerList []cchat.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Session = (*Session)(nil)
|
||||||
|
|
||||||
|
func New(username, sessionID string) *Session {
|
||||||
|
return FromState(shared.NewState(username, sessionID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromState(state *shared.State) *Session {
|
||||||
|
return &Session{
|
||||||
|
State: state,
|
||||||
|
ServerList: server.AsCChatServers(
|
||||||
|
server.NewServers(state, rand.Intn(35)+10),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) ID() string {
|
||||||
|
return s.State.SessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Name() text.Rich {
|
||||||
|
return text.Plain(s.State.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Disconnect() error {
|
||||||
|
s.State.SessionID = ""
|
||||||
|
s.State.ResetID()
|
||||||
|
|
||||||
|
return internet.SimulateAustralian()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Servers(container cchat.ServersContainer) error {
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(s.ServerList)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) AsIconer() cchat.Iconer {
|
||||||
|
return shared.NewStaticIcon(message.AvatarURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) AsCommander() cchat.Commander {
|
||||||
|
return &Commander{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) AsSessionSaver() cchat.SessionSaver {
|
||||||
|
return s.State
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StaticIcon is a struct that implements cchat.Iconer. It never updates.
|
||||||
|
type StaticIcon struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStaticIcon(url string) StaticIcon {
|
||||||
|
return StaticIcon{url}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (icn StaticIcon) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to query for icon")
|
||||||
|
}
|
||||||
|
|
||||||
|
iconer.SetIcon(icn.URL)
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { rand.Seed(time.Now().UnixNano()) }
|
||||||
|
|
||||||
|
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||||
|
var ErrInvalidSession = errors.New("invalid session")
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
SessionID string
|
||||||
|
Username string
|
||||||
|
|
||||||
|
lastID uint32 // used for generation
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.SessionSaver = (*State)(nil)
|
||||||
|
|
||||||
|
func NewState(username, sessionID string) *State {
|
||||||
|
var state = &State{Username: username, SessionID: sessionID}
|
||||||
|
if sessionID == "" {
|
||||||
|
state.SessionID = strconv.FormatUint(rand.Uint64(), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func RestoreState(store map[string]string) (*State, error) {
|
||||||
|
sID, ok := store["sessionID"]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidSession
|
||||||
|
}
|
||||||
|
|
||||||
|
un, ok := store["username"]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidSession
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewState(un, sID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) NextID() uint32 {
|
||||||
|
return atomic.AddUint32(&s.lastID, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetID resets the atomic ID counter.
|
||||||
|
func (s *State) ResetID() {
|
||||||
|
atomic.StoreUint32(&s.lastID, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) SaveSession() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"sessionID": s.SessionID,
|
||||||
|
"username": s.Username,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package typing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/internet"
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Typer struct {
|
||||||
|
message.Author
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Typer = (*Typer)(nil)
|
||||||
|
|
||||||
|
func NewTyper(a message.Author) *Typer {
|
||||||
|
return &Typer{Author: a, time: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomTyper() *Typer {
|
||||||
|
return &Typer{
|
||||||
|
Author: message.RandomAuthor(),
|
||||||
|
time: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Typer) Time() time.Time {
|
||||||
|
return t.time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscriber struct {
|
||||||
|
self message.Author
|
||||||
|
incoming chan message.Author
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubscriber(self message.Author) Subscriber {
|
||||||
|
return Subscriber{
|
||||||
|
self: self,
|
||||||
|
incoming: make(chan message.Author),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts Subscriber) TriggerTyping(author message.Author) {
|
||||||
|
ts.incoming <- author
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts Subscriber) TypingSubscribe(ti cchat.TypingContainer) (func(), 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 := <-ts.incoming:
|
||||||
|
ti.AddTyper(NewTyper(author))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return func() { close(stopch) }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typing sleeps and returns possibly an error.
|
||||||
|
func (ts Subscriber) Typing() error {
|
||||||
|
if err := internet.SimulateAustralian(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ts.TypingNow()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypingNow sends a typing event immediately.
|
||||||
|
func (ts Subscriber) TypingNow() {
|
||||||
|
ts.TriggerTyping(ts.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypingTimeout returns 5 seconds.
|
||||||
|
func (ts Subscriber) TypingTimeout() time.Duration {
|
||||||
|
return 5 * time.Second
|
||||||
|
}
|
44
internet.go
44
internet.go
|
@ -1,44 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
152
message.go
152
message.go
|
@ -1,152 +0,0 @@
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/diamondburned/aqs"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-mock/segments"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
|
|
||||||
_ "github.com/diamondburned/aqs/data"
|
|
||||||
"github.com/diamondburned/aqs/incr"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 parseID(id string) (uint32, error) {
|
|
||||||
i, err := strconv.Atoi(id)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint32(i), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MessageHeader) ID() string {
|
|
||||||
return strconv.Itoa(int(m.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MessageHeader) Time() time.Time {
|
|
||||||
return m.time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
MessageHeader
|
|
||||||
author Author
|
|
||||||
content string
|
|
||||||
nonce string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.MessageCreate = (*Message)(nil)
|
|
||||||
_ cchat.MessageUpdate = (*Message)(nil)
|
|
||||||
_ cchat.MessageDelete = (*Message)(nil)
|
|
||||||
_ cchat.MessageNonce = (*Message)(nil)
|
|
||||||
_ cchat.MessageMentioned = (*Message)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func newEmptyMessage(id uint32, author Author) Message {
|
|
||||||
return Message{
|
|
||||||
MessageHeader: MessageHeader{id: id},
|
|
||||||
author: author,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRandomMessage(id uint32, author Author) Message {
|
|
||||||
return Message{
|
|
||||||
MessageHeader: MessageHeader{id: id, time: time.Now()},
|
|
||||||
author: author,
|
|
||||||
content: incr.RandomQuote(author.char),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func echoMessage(sendable cchat.SendableMessage, id uint32, author Author) Message {
|
|
||||||
var echo = Message{
|
|
||||||
MessageHeader: MessageHeader{id: id, time: time.Now()},
|
|
||||||
author: author,
|
|
||||||
content: sendable.Content(),
|
|
||||||
}
|
|
||||||
if noncer, ok := sendable.(cchat.MessageNonce); ok {
|
|
||||||
echo.nonce = noncer.Nonce()
|
|
||||||
}
|
|
||||||
return echo
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomMessage(id uint32) Message {
|
|
||||||
return randomMessageWithAuthor(id, randomAuthor())
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomMessageWithAuthor(id uint32, author Author) Message {
|
|
||||||
return Message{
|
|
||||||
MessageHeader: MessageHeader{id: id, time: time.Now()},
|
|
||||||
author: author,
|
|
||||||
content: incr.RandomQuote(author.char),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Message) Author() cchat.MessageAuthor {
|
|
||||||
return m.author
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Message) Content() text.Rich {
|
|
||||||
return text.Rich{Content: m.content}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Message) Nonce() string {
|
|
||||||
return m.nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mentioned is true when the message content contains the author's name.
|
|
||||||
func (m Message) Mentioned() bool {
|
|
||||||
// hack
|
|
||||||
return strings.Contains(m.content, m.author.name.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Author struct {
|
|
||||||
name text.Rich
|
|
||||||
char aqs.Character
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.MessageAuthor = (*Author)(nil)
|
|
||||||
_ cchat.MessageAuthorAvatar = (*Author)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func newAuthor(name text.Rich) Author {
|
|
||||||
return Author{name: name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomAuthor() Author {
|
|
||||||
var char = aqs.RandomCharacter()
|
|
||||||
return Author{
|
|
||||||
char: char,
|
|
||||||
name: text.Rich{
|
|
||||||
Content: char.Name,
|
|
||||||
Segments: []text.Segment{segments.NewColorful(char.Name, char.NameColor())},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Author) ID() string {
|
|
||||||
return a.name.Content
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Author) Name() text.Rich {
|
|
||||||
return a.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Author) Avatar() string {
|
|
||||||
if a.char.ImageURL != "" {
|
|
||||||
return a.char.ImageURL
|
|
||||||
}
|
|
||||||
return avatarURL
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Package mock contains a mock cchat backend.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat-mock/internal/service"
|
||||||
|
"github.com/diamondburned/cchat/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
services.RegisterService(&service.Service{})
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,63 +14,78 @@ func init() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
type Colored struct {
|
type ColoredSegment struct {
|
||||||
strlen int
|
empty.TextSegment
|
||||||
color uint32
|
strlen int
|
||||||
|
colored Colored
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var _ text.Segment = (*ColoredSegment)(nil)
|
||||||
_ text.Colorer = (*Colored)(nil)
|
|
||||||
_ text.Segment = (*Colored)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewColored(str string, color uint32) Colored {
|
func NewColoredSegment(str string, color uint32) ColoredSegment {
|
||||||
return Colored{len(str), color}
|
return ColoredSegment{
|
||||||
|
strlen: len(str),
|
||||||
|
colored: NewColored(color),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRandomColored(str string) Colored {
|
func NewRandomColoredSegment(str string) ColoredSegment {
|
||||||
return Colored{len(str), RandomColor()}
|
return ColoredSegment{
|
||||||
|
strlen: len(str),
|
||||||
|
colored: NewRandomColored(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewColorful(str string, color colorful.Color) Colored {
|
func NewColorfulSegment(str string, color colorful.Color) ColoredSegment {
|
||||||
|
return ColoredSegment{
|
||||||
|
strlen: len(str),
|
||||||
|
colored: NewColorful(color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seg ColoredSegment) Bounds() (start, end int) {
|
||||||
|
return 0, seg.strlen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seg ColoredSegment) AsColorer() text.Colorer {
|
||||||
|
return seg.colored
|
||||||
|
}
|
||||||
|
|
||||||
|
type Colored uint32
|
||||||
|
|
||||||
|
var _ text.Colorer = (*Colored)(nil)
|
||||||
|
|
||||||
|
// NewColored makes a new color segment from a string and a 24-bit color.
|
||||||
|
func NewColored(color uint32) Colored {
|
||||||
|
return Colored(color | (0xFF << 24)) // set alpha bits to 0xFF
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandomColored returns a random color segment.
|
||||||
|
func NewRandomColored() Colored {
|
||||||
|
return Colored(RandomColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorful returns a color segment from the given colorful.Color.
|
||||||
|
func NewColorful(color colorful.Color) Colored {
|
||||||
r, g, b := color.RGB255()
|
r, g, b := color.RGB255()
|
||||||
h := (uint32(r) << 16) + (uint32(g) << 8) + (uint32(b))
|
h := (0xFF << 24) + (uint32(r) << 16) + (uint32(g) << 8) + (uint32(b))
|
||||||
return NewColored(str, h)
|
return NewColored(h)
|
||||||
}
|
|
||||||
|
|
||||||
func (color Colored) Bounds() (start, end int) {
|
|
||||||
return 0, color.strlen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (color Colored) Color() uint32 {
|
func (color Colored) Color() uint32 {
|
||||||
return color.color
|
return uint32(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
// var Colors = []uint32{
|
|
||||||
// 0x55cdfc,
|
|
||||||
// 0x609ffb,
|
|
||||||
// 0x6b78fa,
|
|
||||||
// 0x9375f9,
|
|
||||||
// 0xc180f8,
|
|
||||||
// 0xe88af8,
|
|
||||||
// 0xf794e7,
|
|
||||||
// 0xf79ecc,
|
|
||||||
// 0xf7a8b8,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func randomColor() uint32 {
|
|
||||||
// return Colors[rand.Intn(len(Colors))]
|
|
||||||
// }
|
|
||||||
|
|
||||||
var Colors = []uint32{
|
var Colors = []uint32{
|
||||||
0xF5ABBA,
|
0xF5ABBAFF,
|
||||||
0x5ACFFA, // starts here
|
0x5ACFFAFF, // starts here
|
||||||
0xF5ABBA,
|
0xF5ABBAFF,
|
||||||
0xFFFFFF,
|
0xFFFFFFFF,
|
||||||
}
|
}
|
||||||
|
|
||||||
var colorIndex uint32 = 0
|
var colorIndex uint32 = 0
|
||||||
|
|
||||||
|
// RandomColor returns a random 32-bit RGBA color from the known palette.
|
||||||
func RandomColor() uint32 {
|
func RandomColor() uint32 {
|
||||||
i := atomic.AddUint32(&colorIndex, 1) % uint32(len(Colors))
|
i := atomic.AddUint32(&colorIndex, 1) % uint32(len(Colors))
|
||||||
return Colors[i]
|
return Colors[i]
|
||||||
|
|
58
server.go
58
server.go
|
@ -1,58 +0,0 @@
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/Pallinder/go-randomdata"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
session *Session
|
|
||||||
id uint32
|
|
||||||
name string
|
|
||||||
children []cchat.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Server = (*Server)(nil)
|
|
||||||
_ cchat.ServerList = (*Server)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (sv *Server) ID() string {
|
|
||||||
return strconv.Itoa(int(sv.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *Server) Name() text.Rich {
|
|
||||||
return text.Rich{Content: sv.name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sv *Server) Servers(container cchat.ServersContainer) error {
|
|
||||||
// IO time.
|
|
||||||
if err := simulateAustralianInternet(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(sv.children)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateServers(s *Session) []cchat.Server {
|
|
||||||
return generateServers(s, rand.Intn(45)+2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateServers(s *Session, amount int) []cchat.Server {
|
|
||||||
var servers = make([]cchat.Server, amount)
|
|
||||||
for i := range servers {
|
|
||||||
servers[i] = &Server{
|
|
||||||
session: s,
|
|
||||||
id: atomic.AddUint32(&s.lastid, 1),
|
|
||||||
name: randomdata.Noun(),
|
|
||||||
children: generateChannels(s, rand.Intn(12)+2),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return servers
|
|
||||||
}
|
|
276
service.go
276
service.go
|
@ -1,276 +0,0 @@
|
||||||
// Package mock contains a mock cchat backend.
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Pallinder/go-randomdata"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat/services"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
services.RegisterService(&Service{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
|
||||||
var ErrInvalidSession = errors.New("invalid session")
|
|
||||||
|
|
||||||
type Service struct{}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Service = (*Service)(nil)
|
|
||||||
_ cchat.Configurator = (*Service)(nil)
|
|
||||||
_ cchat.SessionRestorer = (*Service)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s Service) Name() text.Rich {
|
|
||||||
return text.Rich{Content: "Mock"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Service) RestoreSession(storage map[string]string) (cchat.Session, error) {
|
|
||||||
if err := simulateAustralianInternet(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Restore failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
username, ok := storage["username"]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrInvalidSession
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID, ok := storage["sessionID"]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrInvalidSession
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSession(username, sessionID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Service) Authenticate() cchat.Authenticator {
|
|
||||||
return Authenticator{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Authenticator struct{}
|
|
||||||
|
|
||||||
var _ cchat.Authenticator = (*Authenticator)(nil)
|
|
||||||
|
|
||||||
func (Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
|
|
||||||
return []cchat.AuthenticateEntry{
|
|
||||||
{
|
|
||||||
Name: "Username",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Password (ignored)",
|
|
||||||
Secret: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Paragraph (ignored)",
|
|
||||||
Multiline: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Authenticator) Authenticate(form []string) (cchat.Session, error) {
|
|
||||||
// SLOW IO TIME.
|
|
||||||
if err := simulateAustralianInternet(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Authentication failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSession(form[0], ""), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Service) SetConfiguration(config map[string]string) error {
|
|
||||||
for _, err := range []error{
|
|
||||||
// shit code, would not recommend. It's only an ok-ish idea here because
|
|
||||||
// unmarshalConfig() returns ErrInvalidConfigAtField.
|
|
||||||
unmarshalConfig(config, "internet.canFail", &internetCanFail),
|
|
||||||
unmarshalConfig(config, "internet.minLatency", &internetMinLatency),
|
|
||||||
unmarshalConfig(config, "internet.maxLatency", &internetMaxLatency),
|
|
||||||
} {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalConfig(config map[string]string, key string, value interface{}) error {
|
|
||||||
if err := json.Unmarshal([]byte(config[key]), value); err != nil {
|
|
||||||
return &cchat.ErrInvalidConfigAtField{
|
|
||||||
Key: key,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
sesID string
|
|
||||||
username string
|
|
||||||
servers []cchat.Server
|
|
||||||
lastid uint32 // used for generation
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Icon = (*Session)(nil)
|
|
||||||
_ cchat.Session = (*Session)(nil)
|
|
||||||
_ cchat.ServerList = (*Session)(nil)
|
|
||||||
_ cchat.SessionSaver = (*Session)(nil)
|
|
||||||
_ cchat.Commander = (*Session)(nil)
|
|
||||||
_ cchat.CommandCompleter = (*Session)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func newSession(username, sessionID string) *Session {
|
|
||||||
ses := &Session{username: username, sesID: sessionID}
|
|
||||||
ses.servers = GenerateServers(ses)
|
|
||||||
|
|
||||||
if sessionID == "" {
|
|
||||||
ses.sesID = strconv.FormatUint(rand.Uint64(), 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ses
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) ID() string {
|
|
||||||
return s.sesID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Name() text.Rich {
|
|
||||||
return text.Rich{Content: s.username}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Disconnect() error {
|
|
||||||
// Nothing to do here, but emulate errors.
|
|
||||||
return simulateAustralianInternet()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Servers(container cchat.ServersContainer) error {
|
|
||||||
// Simulate slight IO.
|
|
||||||
<-time.After(time.Second)
|
|
||||||
|
|
||||||
container.SetServers(s.servers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
|
||||||
// Simulate IO while ignoring the context.
|
|
||||||
simulateAustralianInternet()
|
|
||||||
|
|
||||||
iconer.SetIcon(avatarURL)
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Save() (map[string]string, error) {
|
|
||||||
return map[string]string{
|
|
||||||
"sessionID": s.sesID,
|
|
||||||
"username": s.username,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) RunCommand(cmds []string) (io.ReadCloser, error) {
|
|
||||||
var r, w = io.Pipe()
|
|
||||||
|
|
||||||
switch cmd := arg(cmds, 0); cmd {
|
|
||||||
case "ls":
|
|
||||||
go func() {
|
|
||||||
fmt.Fprintln(w, "Commands: ls, random")
|
|
||||||
w.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
case "random":
|
|
||||||
// callback used to generate stuff and stream into readcloser
|
|
||||||
var generator func() string
|
|
||||||
// number of times to generate the word
|
|
||||||
var times = 1
|
|
||||||
|
|
||||||
switch arg(cmds, 1) {
|
|
||||||
case "paragraph":
|
|
||||||
generator = randomdata.Paragraph
|
|
||||||
case "noun":
|
|
||||||
generator = randomdata.Noun
|
|
||||||
case "silly_name":
|
|
||||||
generator = randomdata.SillyName
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Usage: random <paragraph|noun|silly_name> [repeat]")
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := arg(cmds, 2); n != "" {
|
|
||||||
i, err := strconv.Atoi(n)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to parse repeat number")
|
|
||||||
}
|
|
||||||
times = i
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
for i := 0; i < times; i++ {
|
|
||||||
// Yes, we're simulating this even in something as trivial as a
|
|
||||||
// command prompt.
|
|
||||||
if err := simulateAustralianInternet(); err != nil {
|
|
||||||
fmt.Fprintln(w, "Error:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(w, generator())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Unknown command: %s", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) CompleteCommand(words []string, i int) []string {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix("ls", words[i]):
|
|
||||||
return []string{"ls"}
|
|
||||||
|
|
||||||
case strings.HasPrefix("random", words[i]):
|
|
||||||
return []string{
|
|
||||||
"random paragraph",
|
|
||||||
"random noun",
|
|
||||||
"random silly_name",
|
|
||||||
}
|
|
||||||
|
|
||||||
case lookbackCheck(words, i, "random", "paragraph"):
|
|
||||||
return []string{"paragraph"}
|
|
||||||
|
|
||||||
case lookbackCheck(words, i, "random", "noun"):
|
|
||||||
return []string{"noun"}
|
|
||||||
|
|
||||||
case lookbackCheck(words, i, "random", "silly_name"):
|
|
||||||
return []string{"silly_name"}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func arg(sl []string, i int) string {
|
|
||||||
if i >= len(sl) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return sl[i]
|
|
||||||
}
|
|
Loading…
Reference in New Issue