mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-12 23:27:17 +03:00
258 lines
5.1 KiB
Go
258 lines
5.1 KiB
Go
package message
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/shazow/ssh-chat/set"
|
|
)
|
|
|
|
const messageBuffer = 5
|
|
const messageTimeout = 5 * time.Second
|
|
const reHighlight = `\b(%s)\b`
|
|
const timestampTimeout = 30 * time.Minute
|
|
|
|
var ErrUserClosed = errors.New("user closed")
|
|
|
|
// User definition, implemented set Item interface and io.Writer
|
|
type User struct {
|
|
Identifier
|
|
Ignored *set.Set
|
|
colorIdx int
|
|
joined time.Time
|
|
msg chan Message
|
|
done chan struct{}
|
|
|
|
screen io.WriteCloser
|
|
closeOnce sync.Once
|
|
|
|
mu sync.Mutex
|
|
config UserConfig
|
|
replyTo *User // Set when user gets a /msg, for replying.
|
|
lastMsg time.Time // When the last message was rendered
|
|
}
|
|
|
|
func NewUser(identity Identifier) *User {
|
|
u := User{
|
|
Identifier: identity,
|
|
config: DefaultUserConfig,
|
|
joined: time.Now(),
|
|
msg: make(chan Message, messageBuffer),
|
|
done: make(chan struct{}),
|
|
Ignored: set.New(),
|
|
}
|
|
u.setColorIdx(rand.Int())
|
|
|
|
return &u
|
|
}
|
|
|
|
func NewUserScreen(identity Identifier, screen io.WriteCloser) *User {
|
|
u := NewUser(identity)
|
|
u.screen = screen
|
|
|
|
return u
|
|
}
|
|
|
|
func (u *User) Joined() time.Time {
|
|
return u.joined
|
|
}
|
|
|
|
func (u *User) Config() UserConfig {
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
return u.config
|
|
}
|
|
|
|
func (u *User) SetConfig(cfg UserConfig) {
|
|
u.mu.Lock()
|
|
u.config = cfg
|
|
u.mu.Unlock()
|
|
}
|
|
|
|
// Rename the user with a new Identifier.
|
|
func (u *User) SetID(id string) {
|
|
u.Identifier.SetID(id)
|
|
u.setColorIdx(rand.Int())
|
|
}
|
|
|
|
// ReplyTo returns the last user that messaged this user.
|
|
func (u *User) ReplyTo() *User {
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
return u.replyTo
|
|
}
|
|
|
|
// SetReplyTo sets the last user to message this user.
|
|
func (u *User) SetReplyTo(user *User) {
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
u.replyTo = user
|
|
}
|
|
|
|
// setColorIdx will set the colorIdx to a specific value, primarily used for
|
|
// testing.
|
|
func (u *User) setColorIdx(idx int) {
|
|
u.colorIdx = idx
|
|
}
|
|
|
|
// Disconnect user, stop accepting messages
|
|
func (u *User) Close() {
|
|
u.closeOnce.Do(func() {
|
|
if u.screen != nil {
|
|
u.screen.Close()
|
|
}
|
|
// close(u.msg) TODO: Close?
|
|
close(u.done)
|
|
})
|
|
}
|
|
|
|
// Consume message buffer into the handler. Will block, should be called in a
|
|
// goroutine.
|
|
func (u *User) Consume() {
|
|
for {
|
|
select {
|
|
case <-u.done:
|
|
return
|
|
case m, ok := <-u.msg:
|
|
if !ok {
|
|
return
|
|
}
|
|
u.HandleMsg(m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Consume one message and stop, mostly for testing
|
|
func (u *User) ConsumeOne() Message {
|
|
return <-u.msg
|
|
}
|
|
|
|
// Check if there are pending messages, used for testing
|
|
func (u *User) HasMessages() bool {
|
|
select {
|
|
case msg := <-u.msg:
|
|
u.msg <- msg
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// SetHighlight sets the highlighting regular expression to match string.
|
|
func (u *User) SetHighlight(s string) error {
|
|
re, err := regexp.Compile(fmt.Sprintf(reHighlight, s))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.mu.Lock()
|
|
u.config.Highlight = re
|
|
u.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (u *User) render(m Message) string {
|
|
cfg := u.Config()
|
|
var out string
|
|
switch m := m.(type) {
|
|
case PublicMsg:
|
|
if u == m.From() {
|
|
out += m.RenderSelf(cfg)
|
|
} else {
|
|
out += m.RenderFor(cfg)
|
|
}
|
|
case *PrivateMsg:
|
|
out += m.Render(cfg.Theme)
|
|
if cfg.Bell {
|
|
out += Bel
|
|
}
|
|
case *CommandMsg:
|
|
out += m.RenderSelf(cfg)
|
|
default:
|
|
out += m.Render(cfg.Theme)
|
|
}
|
|
if cfg.Timeformat != nil {
|
|
ts := m.Timestamp()
|
|
if cfg.Timezone != nil {
|
|
ts = ts.In(cfg.Timezone)
|
|
} else {
|
|
ts = ts.UTC()
|
|
}
|
|
return cfg.Theme.Timestamp(ts.Format(*cfg.Timeformat) + " " + out + Newline)
|
|
}
|
|
return out + Newline
|
|
}
|
|
|
|
// writeMsg renders the message and attempts to write it, will Close the user
|
|
// if it fails.
|
|
func (u *User) writeMsg(m Message) error {
|
|
r := u.render(m)
|
|
_, err := u.screen.Write([]byte(r))
|
|
if err != nil {
|
|
logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
|
|
u.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// HandleMsg will render the message to the screen, blocking.
|
|
func (u *User) HandleMsg(m Message) error {
|
|
u.mu.Lock()
|
|
u.lastMsg = m.Timestamp()
|
|
u.mu.Unlock()
|
|
return u.writeMsg(m)
|
|
}
|
|
|
|
// Add message to consume by user
|
|
func (u *User) Send(m Message) error {
|
|
select {
|
|
case <-u.done:
|
|
return ErrUserClosed
|
|
case u.msg <- m:
|
|
case <-time.After(messageTimeout):
|
|
logger.Printf("Message buffer full, closing: %s", u.Name())
|
|
u.Close()
|
|
return ErrUserClosed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Container for per-user configurations.
|
|
type UserConfig struct {
|
|
Highlight *regexp.Regexp
|
|
Bell bool
|
|
Quiet bool
|
|
Timeformat *string
|
|
Timezone *time.Location
|
|
Theme *Theme
|
|
}
|
|
|
|
// Default user configuration to use
|
|
var DefaultUserConfig UserConfig
|
|
|
|
func init() {
|
|
DefaultUserConfig = UserConfig{
|
|
Bell: true,
|
|
Quiet: false,
|
|
}
|
|
|
|
// TODO: Seed random?
|
|
}
|
|
|
|
// RecentActiveUsers is a slice of *Users that knows how to be sorted by the time of the last message.
|
|
type RecentActiveUsers []*User
|
|
|
|
func (a RecentActiveUsers) Len() int { return len(a) }
|
|
func (a RecentActiveUsers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a RecentActiveUsers) Less(i, j int) bool {
|
|
a[i].mu.Lock()
|
|
defer a[i].mu.Unlock()
|
|
a[j].mu.Lock()
|
|
defer a[j].mu.Unlock()
|
|
return a[i].lastMsg.After(a[j].lastMsg)
|
|
}
|