mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-28 22:32:16 +03:00
Merge 7b3818acc1cf13cf9fce74bc51a7997482cf8ac8 into 227dad7492377557dfc9d395d47255cf9f96ff2f
This commit is contained in:
commit
ce50c4043c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
/build
|
/build
|
||||||
|
/vendor
|
||||||
host_key
|
host_key
|
||||||
host_key.pub
|
host_key.pub
|
||||||
ssh-chat
|
ssh-chat
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/chat/message"
|
"github.com/shazow/ssh-chat/chat/message"
|
||||||
@ -25,6 +26,10 @@ var ErrMissingArg = errors.New("missing argument")
|
|||||||
// The error returned when a command is added without a prefix.
|
// The error returned when a command is added without a prefix.
|
||||||
var ErrMissingPrefix = errors.New("command missing prefix")
|
var ErrMissingPrefix = errors.New("command missing prefix")
|
||||||
|
|
||||||
|
// The error returned when we fail to find a corresponding userMember struct
|
||||||
|
// for an ID. This should not happen, probably a bug somewhere if encountered.
|
||||||
|
var ErrMissingMember = errors.New("failed to find member")
|
||||||
|
|
||||||
// Command is a definition of a handler for a command.
|
// Command is a definition of a handler for a command.
|
||||||
type Command struct {
|
type Command struct {
|
||||||
// The command's key, such as /foo
|
// The command's key, such as /foo
|
||||||
@ -132,8 +137,7 @@ func InitCommands(c *Commands) {
|
|||||||
Prefix: "/exit",
|
Prefix: "/exit",
|
||||||
Help: "Exit the chat.",
|
Help: "Exit the chat.",
|
||||||
Handler: func(room *Room, msg message.CommandMsg) error {
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
||||||
msg.From().Close()
|
return msg.From().(io.Closer).Close()
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
c.Alias("/exit", "/quit")
|
c.Alias("/exit", "/quit")
|
||||||
@ -147,18 +151,16 @@ func InitCommands(c *Commands) {
|
|||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return ErrMissingArg
|
return ErrMissingArg
|
||||||
}
|
}
|
||||||
u := msg.From()
|
member, ok := room.MemberByID(msg.From().ID())
|
||||||
|
|
||||||
member, ok := room.MemberByID(u.ID())
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("failed to find member")
|
return ErrMissingMember
|
||||||
}
|
}
|
||||||
|
|
||||||
oldID := member.ID()
|
oldID := member.ID()
|
||||||
member.SetID(SanitizeName(args[0]))
|
member.SetName(args[0])
|
||||||
err := room.Rename(oldID, member)
|
err := room.Rename(oldID, member)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
member.SetID(oldID)
|
member.SetName(oldID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -183,7 +185,7 @@ func InitCommands(c *Commands) {
|
|||||||
PrefixHelp: "[colors|...]",
|
PrefixHelp: "[colors|...]",
|
||||||
Help: "Set your color theme.",
|
Help: "Set your color theme.",
|
||||||
Handler: func(room *Room, msg message.CommandMsg) error {
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
||||||
user := msg.From()
|
user := msg.From().(Member)
|
||||||
args := msg.Args()
|
args := msg.Args()
|
||||||
cfg := user.Config()
|
cfg := user.Config()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@ -222,7 +224,7 @@ func InitCommands(c *Commands) {
|
|||||||
Prefix: "/quiet",
|
Prefix: "/quiet",
|
||||||
Help: "Silence room announcements.",
|
Help: "Silence room announcements.",
|
||||||
Handler: func(room *Room, msg message.CommandMsg) error {
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
||||||
u := msg.From()
|
u := msg.From().(Member)
|
||||||
cfg := u.Config()
|
cfg := u.Config()
|
||||||
cfg.Quiet = !cfg.Quiet
|
cfg.Quiet = !cfg.Quiet
|
||||||
u.SetConfig(cfg)
|
u.SetConfig(cfg)
|
||||||
@ -260,11 +262,16 @@ func InitCommands(c *Commands) {
|
|||||||
PrefixHelp: "[USER]",
|
PrefixHelp: "[USER]",
|
||||||
Help: "Hide messages from USER, /unignore USER to stop hiding.",
|
Help: "Hide messages from USER, /unignore USER to stop hiding.",
|
||||||
Handler: func(room *Room, msg message.CommandMsg) error {
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
||||||
|
from, ok := room.Member(msg.From())
|
||||||
|
if !ok {
|
||||||
|
return ErrMissingMember
|
||||||
|
}
|
||||||
|
|
||||||
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/ignore"))
|
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/ignore"))
|
||||||
if id == "" {
|
if id == "" {
|
||||||
// Print ignored names, if any.
|
// Print ignored names, if any.
|
||||||
var names []string
|
var names []string
|
||||||
msg.From().Ignored.Each(func(_ string, item set.Item) error {
|
from.Ignored.Each(func(_ string, item set.Item) error {
|
||||||
names = append(names, item.Key())
|
names = append(names, item.Key())
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -288,7 +295,7 @@ func InitCommands(c *Commands) {
|
|||||||
return fmt.Errorf("user not found: %s", id)
|
return fmt.Errorf("user not found: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := msg.From().Ignored.Add(set.Itemize(id, target))
|
err := from.Ignored.Add(set.Itemize(id, target))
|
||||||
if err == set.ErrCollision {
|
if err == set.ErrCollision {
|
||||||
return fmt.Errorf("user already ignored: %s", id)
|
return fmt.Errorf("user already ignored: %s", id)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -304,12 +311,16 @@ func InitCommands(c *Commands) {
|
|||||||
Prefix: "/unignore",
|
Prefix: "/unignore",
|
||||||
PrefixHelp: "USER",
|
PrefixHelp: "USER",
|
||||||
Handler: func(room *Room, msg message.CommandMsg) error {
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
||||||
|
from, ok := room.Member(msg.From())
|
||||||
|
if !ok {
|
||||||
|
return ErrMissingMember
|
||||||
|
}
|
||||||
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore"))
|
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore"))
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return errors.New("must specify user")
|
return errors.New("must specify user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := msg.From().Ignored.Remove(id); err != nil {
|
if err := from.Ignored.Remove(id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
chat/member.go
Normal file
23
chat/member.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/shazow/ssh-chat/chat/message"
|
||||||
|
"github.com/shazow/ssh-chat/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Member is a User with per-Room metadata attached to it.
|
||||||
|
type roomMember struct {
|
||||||
|
Member
|
||||||
|
Ignored *set.Set
|
||||||
|
}
|
||||||
|
|
||||||
|
type Member interface {
|
||||||
|
message.Author
|
||||||
|
|
||||||
|
Config() message.UserConfig
|
||||||
|
SetConfig(message.UserConfig)
|
||||||
|
|
||||||
|
Send(message.Message) error
|
||||||
|
|
||||||
|
SetName(string)
|
||||||
|
}
|
24
chat/message/config.go
Normal file
24
chat/message/config.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// Container for per-user configurations.
|
||||||
|
type UserConfig struct {
|
||||||
|
Highlight *regexp.Regexp
|
||||||
|
Bell bool
|
||||||
|
Quiet bool
|
||||||
|
Theme *Theme
|
||||||
|
Seed int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default user configuration to use
|
||||||
|
var DefaultUserConfig UserConfig
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultUserConfig = UserConfig{
|
||||||
|
Bell: true,
|
||||||
|
Quiet: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Seed random?
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
package message
|
|
||||||
|
|
||||||
// Identifier is an interface that can uniquely identify itself.
|
|
||||||
type Identifier interface {
|
|
||||||
ID() string
|
|
||||||
SetID(string)
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimpleID is a simple Identifier implementation used for testing.
|
|
||||||
type SimpleID string
|
|
||||||
|
|
||||||
// ID returns the ID as a string.
|
|
||||||
func (i SimpleID) ID() string {
|
|
||||||
return string(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetID is a no-op
|
|
||||||
func (i SimpleID) SetID(s string) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the ID
|
|
||||||
func (i SimpleID) Name() string {
|
|
||||||
return i.ID()
|
|
||||||
}
|
|
@ -6,6 +6,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Author interface {
|
||||||
|
ID() string
|
||||||
|
Name() string
|
||||||
|
Color() int
|
||||||
|
}
|
||||||
|
|
||||||
// Message is an interface for messages.
|
// Message is an interface for messages.
|
||||||
type Message interface {
|
type Message interface {
|
||||||
Render(*Theme) string
|
Render(*Theme) string
|
||||||
@ -16,15 +22,15 @@ type Message interface {
|
|||||||
|
|
||||||
type MessageTo interface {
|
type MessageTo interface {
|
||||||
Message
|
Message
|
||||||
To() *User
|
To() Author
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageFrom interface {
|
type MessageFrom interface {
|
||||||
Message
|
Message
|
||||||
From() *User
|
From() Author
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseInput(body string, from *User) Message {
|
func ParseInput(body string, from Author) Message {
|
||||||
m := NewPublicMsg(body, from)
|
m := NewPublicMsg(body, from)
|
||||||
cmd, isCmd := m.ParseCommand()
|
cmd, isCmd := m.ParseCommand()
|
||||||
if isCmd {
|
if isCmd {
|
||||||
@ -69,10 +75,10 @@ func (m Msg) Timestamp() time.Time {
|
|||||||
// PublicMsg is any message from a user sent to the room.
|
// PublicMsg is any message from a user sent to the room.
|
||||||
type PublicMsg struct {
|
type PublicMsg struct {
|
||||||
Msg
|
Msg
|
||||||
from *User
|
from Author
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPublicMsg(body string, from *User) PublicMsg {
|
func NewPublicMsg(body string, from Author) PublicMsg {
|
||||||
return PublicMsg{
|
return PublicMsg{
|
||||||
Msg: Msg{
|
Msg: Msg{
|
||||||
body: body,
|
body: body,
|
||||||
@ -82,7 +88,7 @@ func NewPublicMsg(body string, from *User) PublicMsg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m PublicMsg) From() *User {
|
func (m PublicMsg) From() Author {
|
||||||
return m.from
|
return m.from
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,10 +143,10 @@ func (m PublicMsg) String() string {
|
|||||||
// sender to see the emote.
|
// sender to see the emote.
|
||||||
type EmoteMsg struct {
|
type EmoteMsg struct {
|
||||||
Msg
|
Msg
|
||||||
from *User
|
from Author
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmoteMsg(body string, from *User) *EmoteMsg {
|
func NewEmoteMsg(body string, from Author) *EmoteMsg {
|
||||||
return &EmoteMsg{
|
return &EmoteMsg{
|
||||||
Msg: Msg{
|
Msg: Msg{
|
||||||
body: body,
|
body: body,
|
||||||
@ -161,17 +167,17 @@ func (m EmoteMsg) String() string {
|
|||||||
// PrivateMsg is a message sent to another user, not shown to anyone else.
|
// PrivateMsg is a message sent to another user, not shown to anyone else.
|
||||||
type PrivateMsg struct {
|
type PrivateMsg struct {
|
||||||
PublicMsg
|
PublicMsg
|
||||||
to *User
|
to Author
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPrivateMsg(body string, from *User, to *User) PrivateMsg {
|
func NewPrivateMsg(body string, from Author, to Author) PrivateMsg {
|
||||||
return PrivateMsg{
|
return PrivateMsg{
|
||||||
PublicMsg: NewPublicMsg(body, from),
|
PublicMsg: NewPublicMsg(body, from),
|
||||||
to: to,
|
to: to,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m PrivateMsg) To() *User {
|
func (m PrivateMsg) To() Author {
|
||||||
return m.to
|
return m.to
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,10 +197,10 @@ func (m PrivateMsg) String() string {
|
|||||||
// to anyone else. Usually in response to something, like /help.
|
// to anyone else. Usually in response to something, like /help.
|
||||||
type SystemMsg struct {
|
type SystemMsg struct {
|
||||||
Msg
|
Msg
|
||||||
to *User
|
to Author
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSystemMsg(body string, to *User) *SystemMsg {
|
func NewSystemMsg(body string, to Author) *SystemMsg {
|
||||||
return &SystemMsg{
|
return &SystemMsg{
|
||||||
Msg: Msg{
|
Msg: Msg{
|
||||||
body: body,
|
body: body,
|
||||||
@ -215,7 +221,7 @@ func (m *SystemMsg) String() string {
|
|||||||
return fmt.Sprintf("-> %s", m.body)
|
return fmt.Sprintf("-> %s", m.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SystemMsg) To() *User {
|
func (m *SystemMsg) To() Author {
|
||||||
return m.to
|
return m.to
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ func TestMessage(t *testing.T) {
|
|||||||
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
|
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
u := NewUser(SimpleID("foo"))
|
u := NewUser("foo")
|
||||||
expected = "foo: hello"
|
expected = "foo: hello"
|
||||||
actual = NewPublicMsg("hello", u).String()
|
actual = NewPublicMsg("hello", u).String()
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package chat
|
package message
|
||||||
|
|
||||||
import "regexp"
|
import "regexp"
|
||||||
|
|
||||||
var reStripName = regexp.MustCompile("[^\\w.-]")
|
var reStripName = regexp.MustCompile("[^\\w.-]")
|
||||||
|
|
||||||
const maxLength = 16
|
const maxLength = 16
|
||||||
|
|
||||||
// SanitizeName returns a name with only allowed characters and a reasonable length
|
// SanitizeName returns a name with only allowed characters and a reasonable length
|
||||||
@ -15,10 +16,3 @@ func SanitizeName(s string) string {
|
|||||||
s = s[:nameLength]
|
s = s[:nameLength]
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
var reStripData = regexp.MustCompile("[^[:ascii:]]")
|
|
||||||
|
|
||||||
// SanitizeData returns a string with only allowed characters for client-provided metadata inputs.
|
|
||||||
func SanitizeData(s string) string {
|
|
||||||
return reStripData.ReplaceAllString(s, "")
|
|
||||||
}
|
|
208
chat/message/screen.go
Normal file
208
chat/message/screen.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUserClosed = errors.New("user closed")
|
||||||
|
|
||||||
|
const messageBuffer = 5
|
||||||
|
const messageTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
func BufferedScreen(name string, screen io.WriteCloser) *bufferedScreen {
|
||||||
|
return &bufferedScreen{
|
||||||
|
pipedScreen: PipedScreen(name, screen),
|
||||||
|
msg: make(chan Message, messageBuffer),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PipedScreen(name string, screen io.WriteCloser) *pipedScreen {
|
||||||
|
return &pipedScreen{
|
||||||
|
baseScreen: Screen(name),
|
||||||
|
WriteCloser: screen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandledScreen(name string, handler func(Message) error) *handledScreen {
|
||||||
|
return &handledScreen{
|
||||||
|
baseScreen: Screen(name),
|
||||||
|
handler: handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Screen(name string) *baseScreen {
|
||||||
|
return &baseScreen{
|
||||||
|
user: NewUser(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type handledScreen struct {
|
||||||
|
*baseScreen
|
||||||
|
handler func(Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *handledScreen) Send(m Message) error {
|
||||||
|
return u.handler(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen that pipes messages to an io.WriteCloser
|
||||||
|
type pipedScreen struct {
|
||||||
|
*baseScreen
|
||||||
|
io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *pipedScreen) Send(m Message) error {
|
||||||
|
r := u.render(m)
|
||||||
|
_, err := u.Write([]byte(r))
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
|
||||||
|
u.Close()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// User container that knows about writing to an IO screen.
|
||||||
|
type baseScreen struct {
|
||||||
|
sync.Mutex
|
||||||
|
*user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *baseScreen) Config() UserConfig {
|
||||||
|
u.Lock()
|
||||||
|
defer u.Unlock()
|
||||||
|
return u.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *baseScreen) SetConfig(cfg UserConfig) {
|
||||||
|
u.Lock()
|
||||||
|
u.config = cfg
|
||||||
|
u.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *baseScreen) ID() string {
|
||||||
|
u.Lock()
|
||||||
|
defer u.Unlock()
|
||||||
|
return SanitizeName(u.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *baseScreen) Name() string {
|
||||||
|
u.Lock()
|
||||||
|
defer u.Unlock()
|
||||||
|
return u.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *baseScreen) Joined() time.Time {
|
||||||
|
return u.joined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the user with a new Identifier.
|
||||||
|
func (u *baseScreen) SetName(name string) {
|
||||||
|
u.Lock()
|
||||||
|
u.name = name
|
||||||
|
u.config.Seed = rand.Int()
|
||||||
|
u.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyTo returns the last user that messaged this user.
|
||||||
|
func (u *baseScreen) ReplyTo() Author {
|
||||||
|
u.Lock()
|
||||||
|
defer u.Unlock()
|
||||||
|
return u.replyTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReplyTo sets the last user to message this user.
|
||||||
|
func (u *baseScreen) SetReplyTo(user Author) {
|
||||||
|
// TODO: Use UserConfig.ReplyTo string
|
||||||
|
u.Lock()
|
||||||
|
defer u.Unlock()
|
||||||
|
u.replyTo = user
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHighlight sets the highlighting regular expression to match string.
|
||||||
|
func (u *baseScreen) SetHighlight(s string) error {
|
||||||
|
re, err := regexp.Compile(fmt.Sprintf(reHighlight, s))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.Lock()
|
||||||
|
u.config.Highlight = re
|
||||||
|
u.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *baseScreen) render(m Message) string {
|
||||||
|
cfg := u.Config()
|
||||||
|
switch m := m.(type) {
|
||||||
|
case PublicMsg:
|
||||||
|
return m.RenderFor(cfg) + Newline
|
||||||
|
case PrivateMsg:
|
||||||
|
u.SetReplyTo(m.From())
|
||||||
|
return m.Render(cfg.Theme) + Newline
|
||||||
|
default:
|
||||||
|
return m.Render(cfg.Theme) + Newline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt renders a theme-colorized prompt string.
|
||||||
|
func (u *baseScreen) Prompt() string {
|
||||||
|
name := u.Name()
|
||||||
|
cfg := u.Config()
|
||||||
|
if cfg.Theme != nil {
|
||||||
|
name = cfg.Theme.ColorName(u)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] ", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bufferedScreen is a screen that buffers messages on Send using a channel and a consuming goroutine.
|
||||||
|
type bufferedScreen struct {
|
||||||
|
*pipedScreen
|
||||||
|
closeOnce sync.Once
|
||||||
|
msg chan Message
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *bufferedScreen) Close() error {
|
||||||
|
u.closeOnce.Do(func() {
|
||||||
|
close(u.done)
|
||||||
|
})
|
||||||
|
|
||||||
|
return u.pipedScreen.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message to consume by user
|
||||||
|
func (u *bufferedScreen) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume message buffer into the handler. Will block, should be called in a
|
||||||
|
// goroutine.
|
||||||
|
func (u *bufferedScreen) Consume() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-u.done:
|
||||||
|
return
|
||||||
|
case m, ok := <-u.msg:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Pass on to unbuffered screen.
|
||||||
|
u.pipedScreen.Send(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -127,12 +127,12 @@ func (t Theme) ID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Colorize name string given some index
|
// Colorize name string given some index
|
||||||
func (t Theme) ColorName(u *User) string {
|
func (t Theme) ColorName(u Author) string {
|
||||||
if t.names == nil {
|
if t.names == nil {
|
||||||
return u.Name()
|
return u.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.names.Get(u.colorIdx).Format(u.Name())
|
return t.names.Get(u.Color()).Format(u.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colorize the PM string
|
// Colorize the PM string
|
||||||
|
@ -51,8 +51,8 @@ func TestTheme(t *testing.T) {
|
|||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
u := NewUser(SimpleID("foo"))
|
u := NewUser("foo")
|
||||||
u.colorIdx = 4
|
u.config.Seed = 4
|
||||||
actual = colorTheme.ColorName(u)
|
actual = colorTheme.ColorName(u)
|
||||||
expected = "\033[38;05;5mfoo\033[0m"
|
expected = "\033[38;05;5mfoo\033[0m"
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
|
@ -1,208 +1,44 @@
|
|||||||
package message
|
package message
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"regexp"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/set"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const messageBuffer = 5
|
|
||||||
const messageTimeout = 5 * time.Second
|
|
||||||
const reHighlight = `\b(%s)\b`
|
const reHighlight = `\b(%s)\b`
|
||||||
|
|
||||||
var ErrUserClosed = errors.New("user closed")
|
|
||||||
|
|
||||||
// User definition, implemented set Item interface and io.Writer
|
// User definition, implemented set Item interface and io.Writer
|
||||||
type User struct {
|
type user struct {
|
||||||
Identifier
|
|
||||||
Ignored *set.Set
|
|
||||||
colorIdx int
|
|
||||||
joined time.Time
|
joined time.Time
|
||||||
msg chan Message
|
|
||||||
done chan struct{}
|
|
||||||
|
|
||||||
screen io.WriteCloser
|
name string
|
||||||
closeOnce sync.Once
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
config UserConfig
|
config UserConfig
|
||||||
replyTo *User // Set when user gets a /msg, for replying.
|
replyTo Author // Set when user gets a /msg, for replying.
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(identity Identifier) *User {
|
func NewUser(name string) *user {
|
||||||
u := User{
|
u := user{
|
||||||
Identifier: identity,
|
name: name,
|
||||||
config: DefaultUserConfig,
|
config: DefaultUserConfig,
|
||||||
joined: time.Now(),
|
joined: time.Now(),
|
||||||
msg: make(chan Message, messageBuffer),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
Ignored: set.New(),
|
|
||||||
}
|
}
|
||||||
u.setColorIdx(rand.Int())
|
u.config.Seed = rand.Int()
|
||||||
|
|
||||||
return &u
|
return &u
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserScreen(identity Identifier, screen io.WriteCloser) *User {
|
func (u *user) Name() string {
|
||||||
u := NewUser(identity)
|
return u.name
|
||||||
u.screen = screen
|
|
||||||
|
|
||||||
return u
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Config() UserConfig {
|
func (u *user) Color() int {
|
||||||
u.mu.Lock()
|
return u.config.Seed
|
||||||
defer u.mu.Unlock()
|
|
||||||
return u.config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) SetConfig(cfg UserConfig) {
|
func (u *user) ID() string {
|
||||||
u.mu.Lock()
|
return SanitizeName(u.name)
|
||||||
u.config = cfg
|
|
||||||
u.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename the user with a new Identifier.
|
func (u *user) Joined() time.Time {
|
||||||
func (u *User) SetID(id string) {
|
return u.joined
|
||||||
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()
|
|
||||||
switch m := m.(type) {
|
|
||||||
case PublicMsg:
|
|
||||||
return m.RenderFor(cfg) + Newline
|
|
||||||
case PrivateMsg:
|
|
||||||
u.SetReplyTo(m.From())
|
|
||||||
return m.Render(cfg.Theme) + Newline
|
|
||||||
default:
|
|
||||||
return m.Render(cfg.Theme) + Newline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleMsg will render the message to the screen, blocking.
|
|
||||||
func (u *User) HandleMsg(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
Theme *Theme
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default user configuration to use
|
|
||||||
var DefaultUserConfig UserConfig
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
DefaultUserConfig = UserConfig{
|
|
||||||
Bell: true,
|
|
||||||
Quiet: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Seed random?
|
|
||||||
}
|
}
|
||||||
|
@ -9,16 +9,19 @@ func TestMakeUser(t *testing.T) {
|
|||||||
var actual, expected []byte
|
var actual, expected []byte
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &MockScreen{}
|
||||||
u := NewUserScreen(SimpleID("foo"), s)
|
u := PipedScreen("foo", s)
|
||||||
|
|
||||||
m := NewAnnounceMsg("hello")
|
m := NewAnnounceMsg("hello")
|
||||||
|
|
||||||
defer u.Close()
|
defer u.Close()
|
||||||
u.Send(m)
|
err := u.Send(m)
|
||||||
u.HandleMsg(u.ConsumeOne())
|
if err != nil {
|
||||||
|
t.Fatalf("failed to send: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
s.Read(&actual)
|
s.Read(&actual)
|
||||||
expected = []byte(m.String() + Newline)
|
expected = []byte(m.String() + Newline)
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
chat/room.go
62
chat/room.go
@ -21,11 +21,6 @@ var ErrRoomClosed = errors.New("room closed")
|
|||||||
// as empty string.
|
// as empty string.
|
||||||
var ErrInvalidName = errors.New("invalid name")
|
var ErrInvalidName = errors.New("invalid name")
|
||||||
|
|
||||||
// Member is a User with per-Room metadata attached to it.
|
|
||||||
type Member struct {
|
|
||||||
*message.User
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room definition, also a Set of User Items
|
// Room definition, also a Set of User Items
|
||||||
type Room struct {
|
type Room struct {
|
||||||
topic string
|
topic string
|
||||||
@ -58,14 +53,10 @@ func (r *Room) SetCommands(commands Commands) {
|
|||||||
r.commands = commands
|
r.commands = commands
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the room and all the users it contains.
|
// Close the room
|
||||||
func (r *Room) Close() {
|
func (r *Room) Close() {
|
||||||
r.closeOnce.Do(func() {
|
r.closeOnce.Do(func() {
|
||||||
r.closed = true
|
r.closed = true
|
||||||
r.Members.Each(func(_ string, item set.Item) error {
|
|
||||||
item.Value().(*Member).Close()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
r.Members.Clear()
|
r.Members.Clear()
|
||||||
close(r.broadcast)
|
close(r.broadcast)
|
||||||
})
|
})
|
||||||
@ -87,20 +78,21 @@ func (r *Room) HandleMsg(m message.Message) {
|
|||||||
go r.HandleMsg(m)
|
go r.HandleMsg(m)
|
||||||
}
|
}
|
||||||
case message.MessageTo:
|
case message.MessageTo:
|
||||||
user := m.To()
|
user := m.To().(Member)
|
||||||
user.Send(m)
|
user.Send(m)
|
||||||
default:
|
default:
|
||||||
fromMsg, skip := m.(message.MessageFrom)
|
fromMsg, skip := m.(message.MessageFrom)
|
||||||
var skipUser *message.User
|
var skipUser Member
|
||||||
if skip {
|
if skip {
|
||||||
skipUser = fromMsg.From()
|
skipUser = fromMsg.From().(Member)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.history.Add(m)
|
r.history.Add(m)
|
||||||
r.Members.Each(func(_ string, item set.Item) (err error) {
|
r.Members.Each(func(k string, item set.Item) (err error) {
|
||||||
user := item.Value().(*Member).User
|
roomMember := item.Value().(*roomMember)
|
||||||
|
user := roomMember.Member
|
||||||
|
|
||||||
if fromMsg != nil && user.Ignored.In(fromMsg.From().ID()) {
|
if fromMsg != nil && fromMsg.From() != nil && roomMember.Ignored.In(fromMsg.From().ID()) {
|
||||||
// Skip because ignored
|
// Skip because ignored
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -135,31 +127,34 @@ func (r *Room) Send(m message.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// History feeds the room's recent message history to the user's handler.
|
// History feeds the room's recent message history to the user's handler.
|
||||||
func (r *Room) History(u *message.User) {
|
func (r *Room) History(m Member) {
|
||||||
for _, m := range r.history.Get(historyLen) {
|
for _, msg := range r.history.Get(historyLen) {
|
||||||
u.Send(m)
|
m.Send(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join the room as a user, will announce.
|
// Join the room as a user, will announce.
|
||||||
func (r *Room) Join(u *message.User) (*Member, error) {
|
func (r *Room) Join(m Member) (*roomMember, error) {
|
||||||
// TODO: Check if closed
|
// TODO: Check if closed
|
||||||
if u.ID() == "" {
|
if m.ID() == "" {
|
||||||
return nil, ErrInvalidName
|
return nil, ErrInvalidName
|
||||||
}
|
}
|
||||||
member := &Member{u}
|
member := &roomMember{
|
||||||
err := r.Members.Add(set.Itemize(u.ID(), member))
|
Member: m,
|
||||||
|
Ignored: set.New(),
|
||||||
|
}
|
||||||
|
err := r.Members.AddNew(set.Itemize(m.ID(), member))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.History(u)
|
r.History(m)
|
||||||
s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.Members.Len())
|
s := fmt.Sprintf("%s joined. (Connected: %d)", m.Name(), r.Members.Len())
|
||||||
r.Send(message.NewAnnounceMsg(s))
|
r.Send(message.NewAnnounceMsg(s))
|
||||||
return member, nil
|
return member, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave the room as a user, will announce. Mostly used during setup.
|
// Leave the room as a user, will announce. Mostly used during setup.
|
||||||
func (r *Room) Leave(u message.Identifier) error {
|
func (r *Room) Leave(u Member) error {
|
||||||
err := r.Members.Remove(u.ID())
|
err := r.Members.Remove(u.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -171,7 +166,7 @@ func (r *Room) Leave(u message.Identifier) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rename member with a new identity. This will not call rename on the member.
|
// Rename member with a new identity. This will not call rename on the member.
|
||||||
func (r *Room) Rename(oldID string, u message.Identifier) error {
|
func (r *Room) Rename(oldID string, u Member) error {
|
||||||
if u.ID() == "" {
|
if u.ID() == "" {
|
||||||
return ErrInvalidName
|
return ErrInvalidName
|
||||||
}
|
}
|
||||||
@ -187,28 +182,29 @@ func (r *Room) Rename(oldID string, u message.Identifier) error {
|
|||||||
|
|
||||||
// Member returns a corresponding Member object to a User if the Member is
|
// Member returns a corresponding Member object to a User if the Member is
|
||||||
// present in this room.
|
// present in this room.
|
||||||
func (r *Room) Member(u *message.User) (*Member, bool) {
|
func (r *Room) Member(u message.Author) (*roomMember, bool) {
|
||||||
m, ok := r.MemberByID(u.ID())
|
m, ok := r.MemberByID(u.ID())
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
// Check that it's the same user
|
// Check that it's the same user
|
||||||
if m.User != u {
|
if m.Member != u {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return m, true
|
return m, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Room) MemberByID(id string) (*Member, bool) {
|
func (r *Room) MemberByID(id string) (*roomMember, bool) {
|
||||||
m, err := r.Members.Get(id)
|
m, err := r.Members.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return m.Value().(*Member), true
|
rm, ok := m.Value().(*roomMember)
|
||||||
|
return rm, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsOp returns whether a user is an operator in this room.
|
// IsOp returns whether a user is an operator in this room.
|
||||||
func (r *Room) IsOp(u *message.User) bool {
|
func (r *Room) IsOp(u message.Author) bool {
|
||||||
return r.Ops.In(u.ID())
|
return r.Ops.In(u.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +224,7 @@ func (r *Room) NamesPrefix(prefix string) []string {
|
|||||||
items := r.Members.ListPrefix(prefix)
|
items := r.Members.ListPrefix(prefix)
|
||||||
names := make([]string, len(items))
|
names := make([]string, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
names[i] = item.Value().(*Member).User.Name()
|
names[i] = item.Value().(*roomMember).Name()
|
||||||
}
|
}
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,23 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/chat/message"
|
"github.com/shazow/ssh-chat/chat/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used for testing
|
type ChannelWriter struct {
|
||||||
type MockScreen struct {
|
Chan chan []byte
|
||||||
buffer []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MockScreen) Write(data []byte) (n int, err error) {
|
func (w *ChannelWriter) Write(data []byte) (n int, err error) {
|
||||||
s.buffer = append(s.buffer, data...)
|
w.Chan <- data
|
||||||
return len(data), nil
|
return len(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MockScreen) Read(p *[]byte) (n int, err error) {
|
func (w *ChannelWriter) Close() error {
|
||||||
*p = s.buffer
|
close(w.Chan)
|
||||||
s.buffer = []byte{}
|
|
||||||
return len(*p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MockScreen) Close() error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,115 +34,6 @@ func TestRoomServe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScreenedUser struct {
|
|
||||||
user *message.User
|
|
||||||
screen *MockScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIgnore(t *testing.T) {
|
|
||||||
var buffer []byte
|
|
||||||
|
|
||||||
ch := NewRoom()
|
|
||||||
go ch.Serve()
|
|
||||||
defer ch.Close()
|
|
||||||
|
|
||||||
// Create 3 users, join the room and clear their screen buffers
|
|
||||||
users := make([]ScreenedUser, 3)
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
screen := &MockScreen{}
|
|
||||||
user := message.NewUserScreen(message.SimpleID(fmt.Sprintf("user%d", i)), screen)
|
|
||||||
users[i] = ScreenedUser{
|
|
||||||
user: user,
|
|
||||||
screen: screen,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := ch.Join(user)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, u := range users {
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
u.user.HandleMsg(u.user.ConsumeOne())
|
|
||||||
u.screen.Read(&buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use some handy variable names for distinguish between roles
|
|
||||||
ignorer := users[0]
|
|
||||||
ignored := users[1]
|
|
||||||
other := users[2]
|
|
||||||
|
|
||||||
// test ignoring unexisting user
|
|
||||||
if err := sendCommand("/ignore test", ignorer, ch, &buffer); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectOutput(t, buffer, "-> Err: user not found: test"+message.Newline)
|
|
||||||
|
|
||||||
// test ignoring existing user
|
|
||||||
if err := sendCommand("/ignore "+ignored.user.Name(), ignorer, ch, &buffer); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectOutput(t, buffer, "-> Ignoring: "+ignored.user.Name()+message.Newline)
|
|
||||||
|
|
||||||
// ignoring the same user twice returns an error message and doesn't add the user twice
|
|
||||||
if err := sendCommand("/ignore "+ignored.user.Name(), ignorer, ch, &buffer); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectOutput(t, buffer, "-> Err: user already ignored: user1"+message.Newline)
|
|
||||||
if ignoredList := ignorer.user.Ignored.ListPrefix(""); len(ignoredList) != 1 {
|
|
||||||
t.Fatalf("should have %d ignored users, has %d", 1, len(ignoredList))
|
|
||||||
}
|
|
||||||
|
|
||||||
// when a message is sent from the ignored user, it is delivered to non-ignoring users
|
|
||||||
ch.Send(message.NewPublicMsg("hello", ignored.user))
|
|
||||||
other.user.HandleMsg(other.user.ConsumeOne())
|
|
||||||
other.screen.Read(&buffer)
|
|
||||||
expectOutput(t, buffer, ignored.user.Name()+": hello"+message.Newline)
|
|
||||||
|
|
||||||
// ensure ignorer doesn't have received any message
|
|
||||||
if ignorer.user.HasMessages() {
|
|
||||||
t.Fatal("should not have messages")
|
|
||||||
}
|
|
||||||
|
|
||||||
// `/ignore` returns a list of ignored users
|
|
||||||
if err := sendCommand("/ignore", ignorer, ch, &buffer); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectOutput(t, buffer, "-> 1 ignored: "+ignored.user.Name()+message.Newline)
|
|
||||||
|
|
||||||
// `/unignore [USER]` removes the user from ignored ones
|
|
||||||
if err := sendCommand("/unignore "+ignored.user.Name(), users[0], ch, &buffer); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectOutput(t, buffer, "-> No longer ignoring: user1"+message.Newline)
|
|
||||||
|
|
||||||
if err := sendCommand("/ignore", users[0], ch, &buffer); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectOutput(t, buffer, "-> 0 users ignored."+message.Newline)
|
|
||||||
|
|
||||||
if ignoredList := ignorer.user.Ignored.ListPrefix(""); len(ignoredList) != 0 {
|
|
||||||
t.Fatalf("should have %d ignored users, has %d", 0, len(ignoredList))
|
|
||||||
}
|
|
||||||
|
|
||||||
// after unignoring a user, its messages can be received again
|
|
||||||
ch.Send(message.NewPublicMsg("hello again!", ignored.user))
|
|
||||||
|
|
||||||
// give some time for the channel to get the message
|
|
||||||
time.Sleep(100)
|
|
||||||
|
|
||||||
// ensure ignorer has received the message
|
|
||||||
if !ignorer.user.HasMessages() {
|
|
||||||
// FIXME: This is flaky :/
|
|
||||||
t.Fatal("should have messages")
|
|
||||||
}
|
|
||||||
ignorer.user.HandleMsg(ignorer.user.ConsumeOne())
|
|
||||||
ignorer.screen.Read(&buffer)
|
|
||||||
expectOutput(t, buffer, ignored.user.Name()+": hello again!"+message.Newline)
|
|
||||||
}
|
|
||||||
|
|
||||||
func expectOutput(t *testing.T, buffer []byte, expected string) {
|
func expectOutput(t *testing.T, buffer []byte, expected string) {
|
||||||
bytes := []byte(expected)
|
bytes := []byte(expected)
|
||||||
if !reflect.DeepEqual(buffer, bytes) {
|
if !reflect.DeepEqual(buffer, bytes) {
|
||||||
@ -159,24 +41,13 @@ func expectOutput(t *testing.T, buffer []byte, expected string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendCommand(cmd string, mock ScreenedUser, room *Room, buffer *[]byte) error {
|
|
||||||
msg, ok := message.NewPublicMsg(cmd, mock.user).ParseCommand()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("cannot parse command message")
|
|
||||||
}
|
|
||||||
|
|
||||||
room.Send(msg)
|
|
||||||
mock.user.HandleMsg(mock.user.ConsumeOne())
|
|
||||||
mock.screen.Read(buffer)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRoomJoin(t *testing.T) {
|
func TestRoomJoin(t *testing.T) {
|
||||||
var expected, actual []byte
|
var expected, actual []byte
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &ChannelWriter{
|
||||||
u := message.NewUserScreen(message.SimpleID("foo"), s)
|
Chan: make(chan []byte),
|
||||||
|
}
|
||||||
|
u := message.PipedScreen("foo", s)
|
||||||
|
|
||||||
ch := NewRoom()
|
ch := NewRoom()
|
||||||
go ch.Serve()
|
go ch.Serve()
|
||||||
@ -187,32 +58,121 @@ func TestRoomJoin(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
|
expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch.Send(message.NewSystemMsg("hello", u))
|
ch.Send(message.NewSystemMsg("hello", u))
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte("-> hello" + message.Newline)
|
expected = []byte("-> hello" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch.Send(message.ParseInput("/me says hello.", u))
|
ch.Send(message.ParseInput("/me says hello.", u))
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte("** foo says hello." + message.Newline)
|
expected = []byte("** foo says hello." + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnore(t *testing.T) {
|
||||||
|
ch := NewRoom()
|
||||||
|
go ch.Serve()
|
||||||
|
defer ch.Close()
|
||||||
|
|
||||||
|
addUser := func(name string) (message.Author, <-chan []byte) {
|
||||||
|
s := &ChannelWriter{
|
||||||
|
Chan: make(chan []byte, 3),
|
||||||
|
}
|
||||||
|
u := message.PipedScreen(name, s)
|
||||||
|
u.SetConfig(message.UserConfig{
|
||||||
|
Quiet: true,
|
||||||
|
})
|
||||||
|
ch.Join(u)
|
||||||
|
return u, s.Chan
|
||||||
|
}
|
||||||
|
|
||||||
|
u_foo, m_foo := addUser("foo")
|
||||||
|
u_bar, m_bar := addUser("bar")
|
||||||
|
u_quux, m_quux := addUser("quux")
|
||||||
|
|
||||||
|
var expected, actual []byte
|
||||||
|
|
||||||
|
// foo ignores bar, quux hears both
|
||||||
|
ch.Send(message.ParseInput("/ignore bar", u_foo))
|
||||||
|
expected = []byte("-> Ignoring: bar" + message.Newline)
|
||||||
|
actual = <-m_foo
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bar and quux sends a message, quux hears bar, foo only hears quux
|
||||||
|
ch.Send(message.ParseInput("i am bar", u_bar))
|
||||||
|
ch.Send(message.ParseInput("i am quux", u_quux))
|
||||||
|
|
||||||
|
expected = []byte("bar: i am bar" + message.Newline)
|
||||||
|
actual = <-m_quux
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = []byte("quux: i am quux" + message.Newline)
|
||||||
|
actual = <-m_bar
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
actual = <-m_foo
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// foo sends a message, both quux and bar hear it
|
||||||
|
ch.Send(message.ParseInput("i am foo", u_foo))
|
||||||
|
expected = []byte("foo: i am foo" + message.Newline)
|
||||||
|
|
||||||
|
actual = <-m_quux
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
actual = <-m_bar
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm foo's message queue is still empty
|
||||||
|
select {
|
||||||
|
case actual = <-m_foo:
|
||||||
|
t.Errorf("foo's message queue is not empty: %q", actual)
|
||||||
|
default:
|
||||||
|
// Pass.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unignore and listen to bar again.
|
||||||
|
ch.Send(message.ParseInput("/unignore bar", u_foo))
|
||||||
|
expected = []byte("-> No longer ignoring: bar" + message.Newline)
|
||||||
|
actual = <-m_foo
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.Send(message.ParseInput("i am bar again", u_bar))
|
||||||
|
expected = []byte("bar: i am bar again" + message.Newline)
|
||||||
|
actual = <-m_foo
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
|
func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
|
||||||
u := message.NewUser(message.SimpleID("foo"))
|
msgs := make(chan message.Message)
|
||||||
|
u := message.HandledScreen("foo", func(m message.Message) error {
|
||||||
|
msgs <- m
|
||||||
|
return nil
|
||||||
|
})
|
||||||
u.SetConfig(message.UserConfig{
|
u.SetConfig(message.UserConfig{
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
})
|
})
|
||||||
@ -225,19 +185,12 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the initial Join message
|
|
||||||
<-ch.broadcast
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
/*
|
for msg := range msgs {
|
||||||
for {
|
|
||||||
msg := u.ConsumeChan()
|
|
||||||
if _, ok := msg.(*message.AnnounceMsg); ok {
|
if _, ok := msg.(*message.AnnounceMsg); ok {
|
||||||
t.Errorf("Got unexpected `%T`", msg)
|
t.Errorf("Got unexpected `%T`", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
// XXX: Fix this
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Call with an AnnounceMsg and all the other types
|
// Call with an AnnounceMsg and all the other types
|
||||||
@ -248,10 +201,16 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
|
|||||||
ch.HandleMsg(message.NewSystemMsg("hello", u))
|
ch.HandleMsg(message.NewSystemMsg("hello", u))
|
||||||
ch.HandleMsg(message.NewPrivateMsg("hello", u, u))
|
ch.HandleMsg(message.NewPrivateMsg("hello", u, u))
|
||||||
ch.HandleMsg(message.NewPublicMsg("hello", u))
|
ch.HandleMsg(message.NewPublicMsg("hello", u))
|
||||||
|
// Try an ignored one again just in case
|
||||||
|
ch.HandleMsg(message.NewAnnounceMsg("Once more for fun"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoomQuietToggleBroadcasts(t *testing.T) {
|
func TestRoomQuietToggleBroadcasts(t *testing.T) {
|
||||||
u := message.NewUser(message.SimpleID("foo"))
|
msgs := make(chan message.Message)
|
||||||
|
u := message.HandledScreen("foo", func(m message.Message) error {
|
||||||
|
msgs <- m
|
||||||
|
return nil
|
||||||
|
})
|
||||||
u.SetConfig(message.UserConfig{
|
u.SetConfig(message.UserConfig{
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
})
|
})
|
||||||
@ -264,16 +223,13 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the initial Join message
|
|
||||||
<-ch.broadcast
|
|
||||||
|
|
||||||
u.SetConfig(message.UserConfig{
|
u.SetConfig(message.UserConfig{
|
||||||
Quiet: false,
|
Quiet: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
expectedMsg := message.NewAnnounceMsg("Ignored")
|
expectedMsg := message.NewAnnounceMsg("Ignored")
|
||||||
ch.HandleMsg(expectedMsg)
|
go ch.HandleMsg(expectedMsg)
|
||||||
msg := u.ConsumeOne()
|
msg := <-msgs
|
||||||
if _, ok := msg.(*message.AnnounceMsg); !ok {
|
if _, ok := msg.(*message.AnnounceMsg); !ok {
|
||||||
t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg)
|
t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg)
|
||||||
}
|
}
|
||||||
@ -282,9 +238,11 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) {
|
|||||||
Quiet: true,
|
Quiet: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
ch.HandleMsg(message.NewAnnounceMsg("Ignored"))
|
ch.HandleMsg(message.NewAnnounceMsg("Ignored"))
|
||||||
ch.HandleMsg(message.NewSystemMsg("hello", u))
|
ch.HandleMsg(message.NewSystemMsg("hello", u))
|
||||||
msg = u.ConsumeOne()
|
}()
|
||||||
|
msg = <-msgs
|
||||||
if _, ok := msg.(*message.AnnounceMsg); ok {
|
if _, ok := msg.(*message.AnnounceMsg); ok {
|
||||||
t.Errorf("Got unexpected `%T`", msg)
|
t.Errorf("Got unexpected `%T`", msg)
|
||||||
}
|
}
|
||||||
@ -293,8 +251,10 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) {
|
|||||||
func TestQuietToggleDisplayState(t *testing.T) {
|
func TestQuietToggleDisplayState(t *testing.T) {
|
||||||
var expected, actual []byte
|
var expected, actual []byte
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &ChannelWriter{
|
||||||
u := message.NewUserScreen(message.SimpleID("foo"), s)
|
Chan: make(chan []byte),
|
||||||
|
}
|
||||||
|
u := message.PipedScreen("foo", s)
|
||||||
|
|
||||||
ch := NewRoom()
|
ch := NewRoom()
|
||||||
go ch.Serve()
|
go ch.Serve()
|
||||||
@ -305,27 +265,24 @@ func TestQuietToggleDisplayState(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
|
expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch.Send(message.ParseInput("/quiet", u))
|
ch.Send(message.ParseInput("/quiet", u))
|
||||||
|
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte("-> Quiet mode is toggled ON" + message.Newline)
|
expected = []byte("-> Quiet mode is toggled ON" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch.Send(message.ParseInput("/quiet", u))
|
ch.Send(message.ParseInput("/quiet", u))
|
||||||
|
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte("-> Quiet mode is toggled OFF" + message.Newline)
|
expected = []byte("-> Quiet mode is toggled OFF" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
@ -334,8 +291,10 @@ func TestQuietToggleDisplayState(t *testing.T) {
|
|||||||
func TestRoomNames(t *testing.T) {
|
func TestRoomNames(t *testing.T) {
|
||||||
var expected, actual []byte
|
var expected, actual []byte
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &ChannelWriter{
|
||||||
u := message.NewUserScreen(message.SimpleID("foo"), s)
|
Chan: make(chan []byte),
|
||||||
|
}
|
||||||
|
u := message.PipedScreen("foo", s)
|
||||||
|
|
||||||
ch := NewRoom()
|
ch := NewRoom()
|
||||||
go ch.Serve()
|
go ch.Serve()
|
||||||
@ -346,18 +305,16 @@ func TestRoomNames(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
|
expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch.Send(message.ParseInput("/names", u))
|
ch.Send(message.ParseInput("/names", u))
|
||||||
|
|
||||||
u.HandleMsg(u.ConsumeOne())
|
|
||||||
expected = []byte("-> 1 connected: foo" + message.Newline)
|
expected = []byte("-> 1 connected: foo" + message.Newline)
|
||||||
s.Read(&actual)
|
actual = <-s.Chan
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
func TestSet(t *testing.T) {
|
func TestSet(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
s := set.New()
|
s := set.New()
|
||||||
u := message.NewUser(message.SimpleID("foo"))
|
u := message.NewUser("foo")
|
||||||
|
|
||||||
if s.In(u.ID()) {
|
if s.In(u.ID()) {
|
||||||
t.Errorf("Set should be empty.")
|
t.Errorf("Set should be empty.")
|
||||||
@ -25,15 +25,15 @@ func TestSet(t *testing.T) {
|
|||||||
t.Errorf("Set should contain user.")
|
t.Errorf("Set should contain user.")
|
||||||
}
|
}
|
||||||
|
|
||||||
u2 := message.NewUser(message.SimpleID("bar"))
|
u2 := message.NewUser("bar")
|
||||||
err = s.Add(set.Itemize(u2.ID(), u2))
|
err = s.Add(set.Itemize(u2.ID(), u2))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Add(set.Itemize(u2.ID(), u2))
|
err = s.AddNew(set.Itemize(u2.ID(), u2))
|
||||||
if err != set.ErrCollision {
|
if err != set.ErrCollision {
|
||||||
t.Error(err)
|
t.Errorf("expected ErrCollision, got: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
size := s.Len()
|
size := s.Len()
|
||||||
|
45
client.go
Normal file
45
client.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package sshchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shazow/ssh-chat/chat"
|
||||||
|
"github.com/shazow/ssh-chat/chat/message"
|
||||||
|
"github.com/shazow/ssh-chat/sshd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
Member
|
||||||
|
sync.Mutex
|
||||||
|
conns []sshd.Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *client) Connections() []sshd.Connection {
|
||||||
|
return cl.conns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *client) Close() error {
|
||||||
|
// TODO: Stack errors?
|
||||||
|
for _, conn := range cl.conns {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Member interface {
|
||||||
|
chat.Member
|
||||||
|
|
||||||
|
Joined() time.Time
|
||||||
|
Prompt() string
|
||||||
|
ReplyTo() message.Author
|
||||||
|
SetHighlight(string) error
|
||||||
|
SetReplyTo(message.Author)
|
||||||
|
}
|
||||||
|
|
||||||
|
type User interface {
|
||||||
|
Member
|
||||||
|
|
||||||
|
Connections() []sshd.Connection
|
||||||
|
Close() error
|
||||||
|
}
|
182
host.go
182
host.go
@ -18,16 +18,6 @@ import (
|
|||||||
|
|
||||||
const maxInputLength int = 1024
|
const maxInputLength int = 1024
|
||||||
|
|
||||||
// GetPrompt will render the terminal prompt string based on the user.
|
|
||||||
func GetPrompt(user *message.User) string {
|
|
||||||
name := user.Name()
|
|
||||||
cfg := user.Config()
|
|
||||||
if cfg.Theme != nil {
|
|
||||||
name = cfg.Theme.ColorName(user)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[%s] ", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host is the bridge between sshd and chat modules
|
// Host is the bridge between sshd and chat modules
|
||||||
// TODO: Should be easy to add support for multiple rooms, if we want.
|
// TODO: Should be easy to add support for multiple rooms, if we want.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
@ -45,6 +35,7 @@ type Host struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
motd string
|
motd string
|
||||||
count int
|
count int
|
||||||
|
clients map[chat.Member][]client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHost creates a Host on top of an existing listener.
|
// NewHost creates a Host on top of an existing listener.
|
||||||
@ -55,6 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host {
|
|||||||
listener: listener,
|
listener: listener,
|
||||||
commands: chat.Commands{},
|
commands: chat.Commands{},
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
clients: map[chat.Member][]client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make our own commands registry instance.
|
// Make our own commands registry instance.
|
||||||
@ -80,26 +72,14 @@ func (h *Host) SetMotd(motd string) {
|
|||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Host) isOp(conn sshd.Connection) bool {
|
|
||||||
key := conn.PublicKey()
|
|
||||||
if key == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return h.auth.IsOp(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect a specific Terminal to this host and its room.
|
// Connect a specific Terminal to this host and its room.
|
||||||
func (h *Host) Connect(term *sshd.Terminal) {
|
func (h *Host) Connect(term *sshd.Terminal) {
|
||||||
id := NewIdentity(term.Conn)
|
requestedName := term.Conn.Name()
|
||||||
user := message.NewUserScreen(id, term)
|
screen := message.BufferedScreen(requestedName, term)
|
||||||
cfg := user.Config()
|
user := &client{
|
||||||
cfg.Theme = &h.theme
|
Member: screen,
|
||||||
user.SetConfig(cfg)
|
conns: []sshd.Connection{term.Conn},
|
||||||
go user.Consume()
|
}
|
||||||
|
|
||||||
// Close term once user is closed.
|
|
||||||
defer user.Close()
|
|
||||||
defer term.Close()
|
|
||||||
|
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
motd := h.motd
|
motd := h.motd
|
||||||
@ -107,6 +87,16 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
h.count++
|
h.count++
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
cfg := user.Config()
|
||||||
|
cfg.Theme = &h.theme
|
||||||
|
user.SetConfig(cfg)
|
||||||
|
|
||||||
|
// Close term once user is closed.
|
||||||
|
defer screen.Close()
|
||||||
|
defer term.Close()
|
||||||
|
|
||||||
|
go screen.Consume()
|
||||||
|
|
||||||
// Send MOTD
|
// Send MOTD
|
||||||
if motd != "" {
|
if motd != "" {
|
||||||
user.Send(message.NewAnnounceMsg(motd))
|
user.Send(message.NewAnnounceMsg(motd))
|
||||||
@ -115,7 +105,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
member, err := h.Join(user)
|
member, err := h.Join(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Try again...
|
// Try again...
|
||||||
id.SetName(fmt.Sprintf("Guest%d", count))
|
user.SetName(fmt.Sprintf("Guest%d", count))
|
||||||
member, err = h.Join(user)
|
member, err = h.Join(user)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,16 +114,22 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Successfully joined.
|
// Successfully joined.
|
||||||
term.SetPrompt(GetPrompt(user))
|
term.SetPrompt(user.Prompt())
|
||||||
term.AutoCompleteCallback = h.AutoCompleteFunction(user)
|
term.AutoCompleteCallback = h.AutoCompleteFunction(user)
|
||||||
user.SetHighlight(user.Name())
|
user.SetHighlight(user.Name())
|
||||||
|
|
||||||
// Should the user be op'd on join?
|
// Should the user be op'd on join?
|
||||||
if h.isOp(term.Conn) {
|
if key := term.Conn.PublicKey(); key != nil {
|
||||||
h.Room.Ops.Add(set.Itemize(member.ID(), member))
|
authItem, err := h.auth.ops.Get(newAuthKey(key))
|
||||||
|
if err == nil {
|
||||||
|
err = h.Room.Ops.Add(set.Rename(authItem, member.ID()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("[%s] Failed to op: %s", term.Conn.RemoteAddr(), err)
|
||||||
}
|
}
|
||||||
ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
|
|
||||||
|
|
||||||
|
ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
|
||||||
logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name())
|
logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name())
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -172,7 +168,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
//
|
//
|
||||||
// FIXME: This is hacky, how do we improve the API to allow for
|
// FIXME: This is hacky, how do we improve the API to allow for
|
||||||
// this? Chat module shouldn't know about terminals.
|
// this? Chat module shouldn't know about terminals.
|
||||||
term.SetPrompt(GetPrompt(user))
|
term.SetPrompt(user.Prompt())
|
||||||
user.SetHighlight(user.Name())
|
user.SetHighlight(user.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,7 +207,7 @@ func (h *Host) completeCommand(partial string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AutoCompleteFunction returns a callback for terminal autocompletion
|
// AutoCompleteFunction returns a callback for terminal autocompletion
|
||||||
func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
func (h *Host) AutoCompleteFunction(u User) func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
||||||
return func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
return func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
||||||
if key != 9 {
|
if key != 9 {
|
||||||
return
|
return
|
||||||
@ -268,12 +264,13 @@ func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUser returns a message.User based on a name.
|
// GetUser returns a message.User based on a name.
|
||||||
func (h *Host) GetUser(name string) (*message.User, bool) {
|
func (h *Host) GetUser(name string) (User, bool) {
|
||||||
m, ok := h.MemberByID(name)
|
m, ok := h.MemberByID(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return m.User, true
|
u, ok := m.Member.(User)
|
||||||
|
return u, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitCommands adds host-specific commands to a Commands container. These will
|
// InitCommands adds host-specific commands to a Commands container. These will
|
||||||
@ -319,7 +316,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
return errors.New("must specify message")
|
return errors.New("must specify message")
|
||||||
}
|
}
|
||||||
|
|
||||||
target := msg.From().ReplyTo()
|
target := msg.From().(Member).ReplyTo()
|
||||||
if target == nil {
|
if target == nil {
|
||||||
return errors.New("no message to reply to")
|
return errors.New("no message to reply to")
|
||||||
}
|
}
|
||||||
@ -355,13 +352,12 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
return errors.New("user not found")
|
return errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := target.Identifier.(*Identity)
|
|
||||||
var whois string
|
var whois string
|
||||||
switch room.IsOp(msg.From()) {
|
switch room.IsOp(msg.From()) {
|
||||||
case true:
|
case true:
|
||||||
whois = id.WhoisAdmin()
|
whois = whoisAdmin(target)
|
||||||
case false:
|
case false:
|
||||||
whois = id.Whois()
|
whois = whoisPublic(target)
|
||||||
}
|
}
|
||||||
room.Send(message.NewSystemMsg(whois, msg.From()))
|
room.Send(message.NewSystemMsg(whois, msg.From()))
|
||||||
|
|
||||||
@ -387,12 +383,11 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Op commands
|
|
||||||
c.Add(chat.Command{
|
c.Add(chat.Command{
|
||||||
Op: true,
|
Op: true,
|
||||||
Prefix: "/kick",
|
Prefix: "/op",
|
||||||
PrefixHelp: "USER",
|
PrefixHelp: "USER [DURATION]",
|
||||||
Help: "Kick USER from the server.",
|
Help: "Set USER as admin.",
|
||||||
Handler: func(room *chat.Room, msg message.CommandMsg) error {
|
Handler: func(room *chat.Room, msg message.CommandMsg) error {
|
||||||
if !room.IsOp(msg.From()) {
|
if !room.IsOp(msg.From()) {
|
||||||
return errors.New("must be op")
|
return errors.New("must be op")
|
||||||
@ -403,18 +398,36 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
return errors.New("must specify user")
|
return errors.New("must specify user")
|
||||||
}
|
}
|
||||||
|
|
||||||
target, ok := h.GetUser(args[0])
|
var until time.Duration = 0
|
||||||
|
if len(args) > 1 {
|
||||||
|
until, _ = time.ParseDuration(args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := h.GetUser(args[0])
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("user not found")
|
return errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
if until != 0 {
|
||||||
|
room.Ops.Add(set.Expire(set.Keyize(user.ID()), until))
|
||||||
|
} else {
|
||||||
|
room.Ops.Add(set.Keyize(user.ID()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add pubkeys to op
|
||||||
|
/*
|
||||||
|
for _, conn := range user.Connections() {
|
||||||
|
h.auth.Op(conn.PublicKey(), until)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
body := fmt.Sprintf("Made op by %s.", msg.From().Name())
|
||||||
|
room.Send(message.NewSystemMsg(body, user))
|
||||||
|
|
||||||
body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name())
|
|
||||||
room.Send(message.NewAnnounceMsg(body))
|
|
||||||
target.Close()
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Op commands
|
||||||
c.Add(chat.Command{
|
c.Add(chat.Command{
|
||||||
Op: true,
|
Op: true,
|
||||||
Prefix: "/ban",
|
Prefix: "/ban",
|
||||||
@ -441,20 +454,48 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
until, _ = time.ParseDuration(args[1])
|
until, _ = time.ParseDuration(args[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
id := target.Identifier.(*Identity)
|
for _, conn := range target.Connections() {
|
||||||
h.auth.Ban(id.PublicKey(), until)
|
h.auth.Ban(conn.PublicKey(), until)
|
||||||
h.auth.BanAddr(id.RemoteAddr(), until)
|
h.auth.BanAddr(conn.RemoteAddr(), until)
|
||||||
|
}
|
||||||
|
|
||||||
body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
|
body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
|
||||||
room.Send(message.NewAnnounceMsg(body))
|
room.Send(message.NewAnnounceMsg(body))
|
||||||
target.Close()
|
target.Close()
|
||||||
|
|
||||||
logger.Debugf("Banned: \n-> %s", id.Whois())
|
logger.Debugf("Banned: \n-> %s", whoisAdmin(target))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
c.Add(chat.Command{
|
||||||
|
Op: true,
|
||||||
|
Prefix: "/kick",
|
||||||
|
PrefixHelp: "USER",
|
||||||
|
Help: "Kick USER from the server.",
|
||||||
|
Handler: func(room *chat.Room, msg message.CommandMsg) error {
|
||||||
|
if !room.IsOp(msg.From()) {
|
||||||
|
return errors.New("must be op")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := msg.Args()
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("must specify user")
|
||||||
|
}
|
||||||
|
|
||||||
|
target, ok := h.GetUser(args[0])
|
||||||
|
if !ok {
|
||||||
|
return errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name())
|
||||||
|
room.Send(message.NewAnnounceMsg(body))
|
||||||
|
target.(io.Closer).Close()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
c.Add(chat.Command{
|
c.Add(chat.Command{
|
||||||
Op: true,
|
Op: true,
|
||||||
Prefix: "/motd",
|
Prefix: "/motd",
|
||||||
@ -485,39 +526,4 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
c.Add(chat.Command{
|
|
||||||
Op: true,
|
|
||||||
Prefix: "/op",
|
|
||||||
PrefixHelp: "USER [DURATION]",
|
|
||||||
Help: "Set USER as admin.",
|
|
||||||
Handler: func(room *chat.Room, msg message.CommandMsg) error {
|
|
||||||
if !room.IsOp(msg.From()) {
|
|
||||||
return errors.New("must be op")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := msg.Args()
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("must specify user")
|
|
||||||
}
|
|
||||||
|
|
||||||
var until time.Duration = 0
|
|
||||||
if len(args) > 1 {
|
|
||||||
until, _ = time.ParseDuration(args[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
member, ok := room.MemberByID(args[0])
|
|
||||||
if !ok {
|
|
||||||
return errors.New("user not found")
|
|
||||||
}
|
|
||||||
room.Ops.Add(set.Itemize(member.ID(), member))
|
|
||||||
|
|
||||||
id := member.Identifier.(*Identity)
|
|
||||||
h.auth.Op(id.PublicKey(), until)
|
|
||||||
|
|
||||||
body := fmt.Sprintf("Made op by %s.", msg.From().Name())
|
|
||||||
room.Send(message.NewSystemMsg(body, member.User))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
33
host_test.go
33
host_test.go
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
@ -27,9 +28,9 @@ func stripPrompt(s string) string {
|
|||||||
func TestHostGetPrompt(t *testing.T) {
|
func TestHostGetPrompt(t *testing.T) {
|
||||||
var expected, actual string
|
var expected, actual string
|
||||||
|
|
||||||
u := message.NewUser(&Identity{id: "foo"})
|
u := message.Screen("foo")
|
||||||
|
|
||||||
actual = GetPrompt(u)
|
actual = u.Prompt()
|
||||||
expected = "[foo] "
|
expected = "[foo] "
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
@ -38,8 +39,8 @@ func TestHostGetPrompt(t *testing.T) {
|
|||||||
u.SetConfig(message.UserConfig{
|
u.SetConfig(message.UserConfig{
|
||||||
Theme: &message.Themes[0],
|
Theme: &message.Themes[0],
|
||||||
})
|
})
|
||||||
actual = GetPrompt(u)
|
actual = u.Prompt()
|
||||||
expected = "[\033[38;05;88mfoo\033[0m] "
|
expected = "[\033[38;05;1mfoo\033[0m] "
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
t.Errorf("Got: %q; Expected: %q", actual, expected)
|
||||||
}
|
}
|
||||||
@ -63,6 +64,8 @@ func TestHostNameCollision(t *testing.T) {
|
|||||||
|
|
||||||
done := make(chan struct{}, 1)
|
done := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
canary := "canarystring"
|
||||||
|
|
||||||
// First client
|
// First client
|
||||||
go func() {
|
go func() {
|
||||||
err := sshd.ConnectShell(s.Addr().String(), "foo", func(r io.Reader, w io.WriteCloser) error {
|
err := sshd.ConnectShell(s.Addr().String(), "foo", func(r io.Reader, w io.WriteCloser) error {
|
||||||
@ -92,8 +95,10 @@ func TestHostNameCollision(t *testing.T) {
|
|||||||
t.Errorf("Got %q; expected %q", actual, expected)
|
t.Errorf("Got %q; expected %q", actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, canary+message.Newline)
|
||||||
|
|
||||||
|
<-done
|
||||||
// Wrap it up.
|
// Wrap it up.
|
||||||
close(done)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -108,15 +113,23 @@ func TestHostNameCollision(t *testing.T) {
|
|||||||
err = sshd.ConnectShell(s.Addr().String(), "foo", func(r io.Reader, w io.WriteCloser) error {
|
err = sshd.ConnectShell(s.Addr().String(), "foo", func(r io.Reader, w io.WriteCloser) error {
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
|
|
||||||
// Consume the initial buffer
|
// Scan until we see our canarystring
|
||||||
scanner.Scan()
|
for scanner.Scan() {
|
||||||
scanner.Scan()
|
s := scanner.Text()
|
||||||
scanner.Scan()
|
if strings.HasSuffix(s, canary) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an empty prompt to allow for a full line scan with EOL.
|
||||||
|
fmt.Fprintf(w, message.Newline)
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
actual := scanner.Text()
|
actual := scanner.Text()
|
||||||
if !strings.HasPrefix(actual, "[Guest1] ") {
|
if !strings.HasPrefix(actual, "[Guest1] ") {
|
||||||
t.Errorf("Second client did not get Guest1 name: %q", actual)
|
t.Errorf("Second client did not get Guest1 name: %q", actual)
|
||||||
}
|
}
|
||||||
|
close(done)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -195,7 +208,7 @@ func TestHostKick(t *testing.T) {
|
|||||||
if member == nil {
|
if member == nil {
|
||||||
return errors.New("failed to load MemberByID")
|
return errors.New("failed to load MemberByID")
|
||||||
}
|
}
|
||||||
host.Room.Ops.Add(set.Itemize(member.ID(), member))
|
host.Room.Ops.Add(set.Keyize(member.ID()))
|
||||||
|
|
||||||
// Block until second client is here
|
// Block until second client is here
|
||||||
connected <- struct{}{}
|
connected <- struct{}{}
|
||||||
|
69
identity.go
69
identity.go
@ -1,69 +0,0 @@
|
|||||||
package sshchat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/shazow/ssh-chat/chat"
|
|
||||||
"github.com/shazow/ssh-chat/chat/message"
|
|
||||||
"github.com/shazow/ssh-chat/sshd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Identity is a container for everything that identifies a client.
|
|
||||||
type Identity struct {
|
|
||||||
sshd.Connection
|
|
||||||
id string
|
|
||||||
created time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIdentity returns a new identity object from an sshd.Connection.
|
|
||||||
func NewIdentity(conn sshd.Connection) *Identity {
|
|
||||||
return &Identity{
|
|
||||||
Connection: conn,
|
|
||||||
id: chat.SanitizeName(conn.Name()),
|
|
||||||
created: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i Identity) ID() string {
|
|
||||||
return i.id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Identity) SetID(id string) {
|
|
||||||
i.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Identity) SetName(name string) {
|
|
||||||
i.SetID(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i Identity) Name() string {
|
|
||||||
return i.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whois returns a whois description for non-admin users.
|
|
||||||
func (i Identity) Whois() string {
|
|
||||||
fingerprint := "(no public key)"
|
|
||||||
if i.PublicKey() != nil {
|
|
||||||
fingerprint = sshd.Fingerprint(i.PublicKey())
|
|
||||||
}
|
|
||||||
return "name: " + i.Name() + message.Newline +
|
|
||||||
" > fingerprint: " + fingerprint + message.Newline +
|
|
||||||
" > client: " + chat.SanitizeData(string(i.ClientVersion())) + message.Newline +
|
|
||||||
" > joined: " + humanize.Time(i.created)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WhoisAdmin returns a whois description for admin users.
|
|
||||||
func (i Identity) WhoisAdmin() string {
|
|
||||||
ip, _, _ := net.SplitHostPort(i.RemoteAddr().String())
|
|
||||||
fingerprint := "(no public key)"
|
|
||||||
if i.PublicKey() != nil {
|
|
||||||
fingerprint = sshd.Fingerprint(i.PublicKey())
|
|
||||||
}
|
|
||||||
return "name: " + i.Name() + message.Newline +
|
|
||||||
" > ip: " + ip + message.Newline +
|
|
||||||
" > fingerprint: " + fingerprint + message.Newline +
|
|
||||||
" > client: " + chat.SanitizeData(string(i.ClientVersion())) + message.Newline +
|
|
||||||
" > joined: " + humanize.Time(i.created)
|
|
||||||
}
|
|
10
sanitize.go
Normal file
10
sanitize.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package sshchat
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
var reStripData = regexp.MustCompile("[^[:ascii:]]")
|
||||||
|
|
||||||
|
// SanitizeData returns a string with only allowed characters for client-provided metadata inputs.
|
||||||
|
func SanitizeData(s string) string {
|
||||||
|
return reStripData.ReplaceAllString(s, "")
|
||||||
|
}
|
18
set/item.go
18
set/item.go
@ -25,6 +25,24 @@ func Itemize(key string, value interface{}) Item {
|
|||||||
return &item{key, value}
|
return &item{key, value}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Keyize(key string) Item {
|
||||||
|
return &item{key, struct{}{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type renamedItem struct {
|
||||||
|
Item
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *renamedItem) Key() string {
|
||||||
|
return item.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename item to a new key, same underlying value.
|
||||||
|
func Rename(item Item, key string) Item {
|
||||||
|
return &renamedItem{Item: item, key: key}
|
||||||
|
}
|
||||||
|
|
||||||
type StringItem string
|
type StringItem string
|
||||||
|
|
||||||
func (item StringItem) Key() string {
|
func (item StringItem) Key() string {
|
||||||
|
43
set/set.go
43
set/set.go
@ -55,6 +55,7 @@ func (s *Set) In(key string) bool {
|
|||||||
s.RUnlock()
|
s.RUnlock()
|
||||||
if ok && item.Value() == nil {
|
if ok && item.Value() == nil {
|
||||||
s.cleanup(key)
|
s.cleanup(key)
|
||||||
|
ok = false
|
||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
@ -66,12 +67,13 @@ func (s *Set) Get(key string) (Item, error) {
|
|||||||
item, ok := s.lookup[key]
|
item, ok := s.lookup[key]
|
||||||
s.RUnlock()
|
s.RUnlock()
|
||||||
|
|
||||||
|
if ok && item.Value() == nil {
|
||||||
|
s.cleanup(key)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrMissing
|
return nil, ErrMissing
|
||||||
}
|
}
|
||||||
if item.Value() == nil {
|
|
||||||
s.cleanup(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
@ -87,10 +89,7 @@ func (s *Set) cleanup(key string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add item to this set if it does not exist already.
|
// Add item to this set if it does not exist already.
|
||||||
func (s *Set) Add(item Item) error {
|
func (s *Set) AddNew(item Item) error {
|
||||||
if item.Value() == nil {
|
|
||||||
return ErrNil
|
|
||||||
}
|
|
||||||
key := s.normalize(item.Key())
|
key := s.normalize(item.Key())
|
||||||
|
|
||||||
s.Lock()
|
s.Lock()
|
||||||
@ -101,7 +100,26 @@ func (s *Set) Add(item Item) error {
|
|||||||
return ErrCollision
|
return ErrCollision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.Value() == nil {
|
||||||
|
delete(s.lookup, key)
|
||||||
|
} else {
|
||||||
s.lookup[key] = item
|
s.lookup[key] = item
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to set, replacing if item already exists.
|
||||||
|
func (s *Set) Add(item Item) error {
|
||||||
|
key := s.normalize(item.Key())
|
||||||
|
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if item.Value() == nil {
|
||||||
|
delete(s.lookup, key)
|
||||||
|
} else {
|
||||||
|
s.lookup[key] = item
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,9 +141,6 @@ func (s *Set) Remove(key string) error {
|
|||||||
// Replace oldKey with a new item, which might be a new key.
|
// Replace oldKey with a new item, which might be a new key.
|
||||||
// Can be used to rename items.
|
// Can be used to rename items.
|
||||||
func (s *Set) Replace(oldKey string, item Item) error {
|
func (s *Set) Replace(oldKey string, item Item) error {
|
||||||
if item.Value() == nil {
|
|
||||||
return ErrNil
|
|
||||||
}
|
|
||||||
newKey := s.normalize(item.Key())
|
newKey := s.normalize(item.Key())
|
||||||
oldKey = s.normalize(oldKey)
|
oldKey = s.normalize(oldKey)
|
||||||
|
|
||||||
@ -140,15 +155,15 @@ func (s *Set) Replace(oldKey string, item Item) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove oldKey
|
// Remove oldKey
|
||||||
_, found = s.lookup[oldKey]
|
|
||||||
if !found {
|
|
||||||
return ErrMissing
|
|
||||||
}
|
|
||||||
delete(s.lookup, oldKey)
|
delete(s.lookup, oldKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.Value() == nil {
|
||||||
|
delete(s.lookup, newKey)
|
||||||
|
} else {
|
||||||
// Add new item
|
// Add new item
|
||||||
s.lookup[newKey] = item
|
s.lookup[newKey] = item
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package set
|
package set
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -21,10 +22,16 @@ func TestSetExpiring(t *testing.T) {
|
|||||||
t.Error("not len 1 after set")
|
t.Error("not len 1 after set")
|
||||||
}
|
}
|
||||||
|
|
||||||
item := &ExpiringItem{nil, time.Now().Add(-time.Nanosecond * 1)}
|
item := &ExpiringItem{StringItem("expired"), time.Now().Add(-time.Nanosecond * 1)}
|
||||||
if !item.Expired() {
|
if !item.Expired() {
|
||||||
t.Errorf("ExpiringItem a nanosec ago is not expiring")
|
t.Errorf("ExpiringItem a nanosec ago is not expiring")
|
||||||
}
|
}
|
||||||
|
if err := s.Add(item); err != nil {
|
||||||
|
t.Fatalf("failed to add item: %s", err)
|
||||||
|
}
|
||||||
|
if s.In("expired") {
|
||||||
|
t.Errorf("expired item is present")
|
||||||
|
}
|
||||||
|
|
||||||
item = &ExpiringItem{nil, time.Now().Add(time.Minute * 5)}
|
item = &ExpiringItem{nil, time.Now().Add(time.Minute * 5)}
|
||||||
if item.Expired() {
|
if item.Expired() {
|
||||||
@ -74,8 +81,13 @@ func TestSetExpiring(t *testing.T) {
|
|||||||
t.Errorf("failed to get barbar: %s", err)
|
t.Errorf("failed to get barbar: %s", err)
|
||||||
}
|
}
|
||||||
b := s.ListPrefix("b")
|
b := s.ListPrefix("b")
|
||||||
if len(b) != 2 || b[0].Key() != "bar" || b[1].Key() != "barbar" {
|
if len(b) != 2 {
|
||||||
t.Errorf("b-prefix incorrect: %q", b)
|
t.Errorf("b-prefix incorrect number of results: %d", len(b))
|
||||||
|
}
|
||||||
|
for i, item := range b {
|
||||||
|
if !strings.HasPrefix(item.Key(), "b") {
|
||||||
|
t.Errorf("item %d does not have b prefix: %s", i, item.Key())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Remove("bar"); err != nil {
|
if err := s.Remove("bar"); err != nil {
|
||||||
|
39
whois.go
Normal file
39
whois.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package sshchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
humanize "github.com/dustin/go-humanize"
|
||||||
|
"github.com/shazow/ssh-chat/chat/message"
|
||||||
|
"github.com/shazow/ssh-chat/sshd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helpers for printing whois messages
|
||||||
|
|
||||||
|
func whoisPublic(u User) string {
|
||||||
|
fingerprint := "(no public key)"
|
||||||
|
// FIXME: Use all connections?
|
||||||
|
conn := u.Connections()[0]
|
||||||
|
if conn.PublicKey() != nil {
|
||||||
|
fingerprint = sshd.Fingerprint(conn.PublicKey())
|
||||||
|
}
|
||||||
|
return "name: " + u.Name() + message.Newline +
|
||||||
|
" > fingerprint: " + fingerprint + message.Newline +
|
||||||
|
" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
|
||||||
|
" > joined: " + humanize.Time(u.Joined())
|
||||||
|
}
|
||||||
|
|
||||||
|
func whoisAdmin(u User) string {
|
||||||
|
// FIXME: Use all connections?
|
||||||
|
conn := u.Connections()[0]
|
||||||
|
ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||||
|
fingerprint := "(no public key)"
|
||||||
|
if conn.PublicKey() != nil {
|
||||||
|
fingerprint = sshd.Fingerprint(conn.PublicKey())
|
||||||
|
}
|
||||||
|
return "name: " + u.Name() + message.Newline +
|
||||||
|
" > ip: " + ip + message.Newline +
|
||||||
|
" > fingerprint: " + fingerprint + message.Newline +
|
||||||
|
" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
|
||||||
|
" > joined: " + humanize.Time(u.Joined())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user