ssh-chat/chat/{message,user,theme,history} -> ssh-chat/chat/message

This commit is contained in:
Andrey Petrov 2015-01-20 15:57:01 -08:00
parent 2ebd77af6a
commit c2adb4d632
21 changed files with 237 additions and 169 deletions

View File

@ -6,6 +6,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"github.com/shazow/ssh-chat/chat/message"
) )
// The error returned when an invalid command is issued. // The error returned when an invalid command is issued.
@ -29,7 +31,7 @@ type Command struct {
PrefixHelp string PrefixHelp string
// If omitted, command is hidden from /help // If omitted, command is hidden from /help
Help string Help string
Handler func(*Room, CommandMsg) error Handler func(*Room, message.CommandMsg) error
// Command requires Op permissions // Command requires Op permissions
Op bool Op bool
} }
@ -59,7 +61,7 @@ func (c Commands) Alias(command string, alias string) error {
} }
// Run executes a command message. // Run executes a command message.
func (c Commands) Run(room *Room, msg CommandMsg) error { func (c Commands) Run(room *Room, msg message.CommandMsg) error {
if msg.From == nil { if msg.From == nil {
return ErrNoOwner return ErrNoOwner
} }
@ -84,9 +86,9 @@ func (c Commands) Help(showOp bool) string {
normal = append(normal, cmd) normal = append(normal, cmd)
} }
} }
help := "Available commands:" + Newline + NewCommandsHelp(normal).String() help := "Available commands:" + message.Newline + NewCommandsHelp(normal).String()
if showOp { if showOp {
help += Newline + "-> Operator commands:" + Newline + NewCommandsHelp(op).String() help += message.Newline + "-> Operator commands:" + message.Newline + NewCommandsHelp(op).String()
} }
return help return help
} }
@ -102,24 +104,24 @@ func init() {
func InitCommands(c *Commands) { func InitCommands(c *Commands) {
c.Add(Command{ c.Add(Command{
Prefix: "/help", Prefix: "/help",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
op := room.IsOp(msg.From()) op := room.IsOp(msg.From())
room.Send(NewSystemMsg(room.commands.Help(op), msg.From())) room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From()))
return nil return nil
}, },
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/me", Prefix: "/me",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
me := strings.TrimLeft(msg.body, "/me") me := strings.TrimLeft(msg.Body(), "/me")
if me == "" { if me == "" {
me = "is at a loss for words." me = "is at a loss for words."
} else { } else {
me = me[1:] me = me[1:]
} }
room.Send(NewEmoteMsg(me, msg.From())) room.Send(message.NewEmoteMsg(me, msg.From()))
return nil return nil
}, },
}) })
@ -127,7 +129,7 @@ func InitCommands(c *Commands) {
c.Add(Command{ c.Add(Command{
Prefix: "/exit", Prefix: "/exit",
Help: "Exit the chat.", Help: "Exit the chat.",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
msg.From().Close() msg.From().Close()
return nil return nil
}, },
@ -138,7 +140,7 @@ func InitCommands(c *Commands) {
Prefix: "/nick", Prefix: "/nick",
PrefixHelp: "NAME", PrefixHelp: "NAME",
Help: "Rename yourself.", Help: "Rename yourself.",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
args := msg.Args() args := msg.Args()
if len(args) != 1 { if len(args) != 1 {
return ErrMissingArg return ErrMissingArg
@ -164,11 +166,11 @@ func InitCommands(c *Commands) {
c.Add(Command{ c.Add(Command{
Prefix: "/names", Prefix: "/names",
Help: "List users who are connected.", Help: "List users who are connected.",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
// TODO: colorize // TODO: colorize
names := room.NamesPrefix("") names := room.NamesPrefix("")
body := fmt.Sprintf("%d connected: %s", len(names), strings.Join(names, ", ")) body := fmt.Sprintf("%d connected: %s", len(names), strings.Join(names, ", "))
room.Send(NewSystemMsg(body, msg.From())) room.Send(message.NewSystemMsg(body, msg.From()))
return nil return nil
}, },
}) })
@ -178,7 +180,7 @@ func InitCommands(c *Commands) {
Prefix: "/theme", Prefix: "/theme",
PrefixHelp: "[mono|colors]", PrefixHelp: "[mono|colors]",
Help: "Set your color theme.", Help: "Set your color theme.",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
user := msg.From() user := msg.From()
args := msg.Args() args := msg.Args()
if len(args) == 0 { if len(args) == 0 {
@ -187,16 +189,16 @@ func InitCommands(c *Commands) {
theme = user.Config.Theme.Id() theme = user.Config.Theme.Id()
} }
body := fmt.Sprintf("Current theme: %s", theme) body := fmt.Sprintf("Current theme: %s", theme)
room.Send(NewSystemMsg(body, user)) room.Send(message.NewSystemMsg(body, user))
return nil return nil
} }
id := args[0] id := args[0]
for _, t := range Themes { for _, t := range message.Themes {
if t.Id() == id { if t.Id() == id {
user.Config.Theme = &t user.Config.Theme = &t
body := fmt.Sprintf("Set theme: %s", id) body := fmt.Sprintf("Set theme: %s", id)
room.Send(NewSystemMsg(body, user)) room.Send(message.NewSystemMsg(body, user))
return nil return nil
} }
} }
@ -207,7 +209,7 @@ func InitCommands(c *Commands) {
c.Add(Command{ c.Add(Command{
Prefix: "/quiet", Prefix: "/quiet",
Help: "Silence room announcements.", Help: "Silence room announcements.",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
u := msg.From() u := msg.From()
u.ToggleQuietMode() u.ToggleQuietMode()
@ -217,7 +219,7 @@ func InitCommands(c *Commands) {
} else { } else {
body = "Quiet mode is toggled OFF" body = "Quiet mode is toggled OFF"
} }
room.Send(NewSystemMsg(body, u)) room.Send(message.NewSystemMsg(body, u))
return nil return nil
}, },
}) })
@ -225,7 +227,7 @@ func InitCommands(c *Commands) {
c.Add(Command{ c.Add(Command{
Prefix: "/slap", Prefix: "/slap",
PrefixHelp: "NAME", PrefixHelp: "NAME",
Handler: func(room *Room, msg CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
var me string var me string
args := msg.Args() args := msg.Args()
if len(args) == 0 { if len(args) == 0 {
@ -234,7 +236,7 @@ func InitCommands(c *Commands) {
me = fmt.Sprintf("slaps %s around a bit with a large trout.", strings.Join(args, " ")) me = fmt.Sprintf("slaps %s around a bit with a large trout.", strings.Join(args, " "))
} }
room.Send(NewEmoteMsg(me, msg.From())) room.Send(message.NewEmoteMsg(me, msg.From()))
return nil return nil
}, },
}) })

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/shazow/ssh-chat/chat/message"
) )
type helpItem struct { type helpItem struct {
@ -54,5 +56,5 @@ func (h help) String() string {
} }
sort.Strings(r) sort.Strings(r)
return strings.Join(r, Newline) return strings.Join(r, message.Newline)
} }

View File

@ -1,4 +1,4 @@
package chat package message
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package chat package message
import "testing" import "testing"

26
chat/message/identity.go Normal file
View File

@ -0,0 +1,26 @@
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()
}

22
chat/message/logger.go Normal file
View File

@ -0,0 +1,22 @@
package message
import "io"
import stdlog "log"
var logger *stdlog.Logger
func SetLogger(w io.Writer) {
flags := stdlog.Flags()
prefix := "[chat/message] "
logger = stdlog.New(w, prefix, flags)
}
type nullWriter struct{}
func (nullWriter) Write(data []byte) (int, error) {
return len(data), nil
}
func init() {
SetLogger(nullWriter{})
}

View File

@ -1,4 +1,4 @@
package chat package message
import ( import (
"fmt" "fmt"
@ -245,7 +245,6 @@ type CommandMsg struct {
*PublicMsg *PublicMsg
command string command string
args []string args []string
room *Room
} }
func (m *CommandMsg) Command() string { func (m *CommandMsg) Command() string {
@ -255,3 +254,7 @@ func (m *CommandMsg) Command() string {
func (m *CommandMsg) Args() []string { func (m *CommandMsg) Args() []string {
return m.args return m.args
} }
func (m *CommandMsg) Body() string {
return m.body
}

View File

@ -1,19 +1,7 @@
package chat package message
import "testing" import "testing"
type testId string
func (i testId) Id() string {
return string(i)
}
func (i testId) SetId(s string) {
// no-op
}
func (i testId) Name() string {
return i.Id()
}
func TestMessage(t *testing.T) { func TestMessage(t *testing.T) {
var expected, actual string var expected, actual string
@ -23,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(testId("foo")) u := NewUser(SimpleId("foo"))
expected = "foo: hello" expected = "foo: hello"
actual = NewPublicMsg("hello", u).String() actual = NewPublicMsg("hello", u).String()
if actual != expected { if actual != expected {

View File

@ -1,4 +1,4 @@
package chat package message
import ( import (
"reflect" "reflect"

View File

@ -1,4 +1,4 @@
package chat package message
import "fmt" import "fmt"

View File

@ -1,4 +1,4 @@
package chat package message
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package chat package message
import ( import (
"errors" "errors"
@ -15,13 +15,6 @@ const reHighlight = `\b(%s)\b`
var ErrUserClosed = errors.New("user closed") var ErrUserClosed = errors.New("user closed")
// Identifier is an interface that can uniquely identify itself.
type Identifier interface {
Id() string
SetId(string)
Name() string
}
// 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 Identifier
@ -106,8 +99,8 @@ func (u *User) Consume(out io.Writer) {
} }
// Consume one message and stop, mostly for testing // Consume one message and stop, mostly for testing
func (u *User) ConsumeOne(out io.Writer) { func (u *User) ConsumeChan() <-chan Message {
u.HandleMsg(<-u.msg, out) return u.msg
} }
// SetHighlight sets the highlighting regular expression to match string. // SetHighlight sets the highlighting regular expression to match string.

View File

@ -1,4 +1,4 @@
package chat package message
import ( import (
"reflect" "reflect"

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"io" "io"
"sync" "sync"
"github.com/shazow/ssh-chat/chat/message"
) )
const historyLen = 20 const historyLen = 20
@ -20,16 +22,16 @@ var ErrInvalidName = errors.New("invalid name")
// Member is a User with per-Room metadata attached to it. // Member is a User with per-Room metadata attached to it.
type Member struct { type Member struct {
*User *message.User
Op bool Op bool
} }
// 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
history *History history *message.History
members *Set members *Set
broadcast chan Message broadcast chan message.Message
commands Commands commands Commands
closed bool closed bool
closeOnce sync.Once closeOnce sync.Once
@ -37,11 +39,11 @@ type Room struct {
// NewRoom creates a new room. // NewRoom creates a new room.
func NewRoom() *Room { func NewRoom() *Room {
broadcast := make(chan Message, roomBuffer) broadcast := make(chan message.Message, roomBuffer)
return &Room{ return &Room{
broadcast: broadcast, broadcast: broadcast,
history: NewHistory(historyLen), history: message.NewHistory(historyLen),
members: NewSet(), members: NewSet(),
commands: *defaultCommands, commands: *defaultCommands,
} }
@ -56,7 +58,7 @@ func (r *Room) SetCommands(commands Commands) {
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(m Identifier) { r.members.Each(func(m Item) {
m.(*Member).Close() m.(*Member).Close()
}) })
r.members.Clear() r.members.Clear()
@ -70,33 +72,33 @@ func (r *Room) SetLogging(out io.Writer) {
} }
// HandleMsg reacts to a message, will block until done. // HandleMsg reacts to a message, will block until done.
func (r *Room) HandleMsg(m Message) { func (r *Room) HandleMsg(m message.Message) {
switch m := m.(type) { switch m := m.(type) {
case *CommandMsg: case *message.CommandMsg:
cmd := *m cmd := *m
err := r.commands.Run(r, cmd) err := r.commands.Run(r, cmd)
if err != nil { if err != nil {
m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from) m := message.NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.From())
go r.HandleMsg(m) go r.HandleMsg(m)
} }
case MessageTo: case message.MessageTo:
user := m.To() user := m.To()
user.Send(m) user.Send(m)
default: default:
fromMsg, skip := m.(MessageFrom) fromMsg, skip := m.(message.MessageFrom)
var skipUser *User var skipUser *message.User
if skip { if skip {
skipUser = fromMsg.From() skipUser = fromMsg.From()
} }
r.history.Add(m) r.history.Add(m)
r.members.Each(func(u Identifier) { r.members.Each(func(u Item) {
user := u.(*Member).User user := u.(*Member).User
if skip && skipUser == user { if skip && skipUser == user {
// Skip // Skip
return return
} }
if _, ok := m.(*AnnounceMsg); ok { if _, ok := m.(*message.AnnounceMsg); ok {
if user.Config.Quiet { if user.Config.Quiet {
// Skip // Skip
return return
@ -116,19 +118,19 @@ func (r *Room) Serve() {
} }
// Send message, buffered by a chan. // Send message, buffered by a chan.
func (r *Room) Send(m Message) { func (r *Room) Send(m message.Message) {
r.broadcast <- m r.broadcast <- m
} }
// 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 *User) { func (r *Room) History(u *message.User) {
for _, m := range r.history.Get(historyLen) { for _, m := range r.history.Get(historyLen) {
u.Send(m) u.Send(m)
} }
} }
// Join the room as a user, will announce. // Join the room as a user, will announce.
func (r *Room) Join(u *User) (*Member, error) { func (r *Room) Join(u *message.User) (*Member, error) {
if r.closed { if r.closed {
return nil, ErrRoomClosed return nil, ErrRoomClosed
} }
@ -142,23 +144,23 @@ func (r *Room) Join(u *User) (*Member, error) {
} }
r.History(u) r.History(u)
s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.members.Len()) s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.members.Len())
r.Send(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 *User) error { func (r *Room) Leave(u message.Identifier) error {
err := r.members.Remove(u) err := r.members.Remove(u)
if err != nil { if err != nil {
return err return err
} }
s := fmt.Sprintf("%s left.", u.Name()) s := fmt.Sprintf("%s left.", u.Name())
r.Send(NewAnnounceMsg(s)) r.Send(message.NewAnnounceMsg(s))
return nil return nil
} }
// 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, identity Identifier) error { func (r *Room) Rename(oldId string, identity message.Identifier) error {
if identity.Id() == "" { if identity.Id() == "" {
return ErrInvalidName return ErrInvalidName
} }
@ -168,13 +170,13 @@ func (r *Room) Rename(oldId string, identity Identifier) error {
} }
s := fmt.Sprintf("%s is now known as %s.", oldId, identity.Id()) s := fmt.Sprintf("%s is now known as %s.", oldId, identity.Id())
r.Send(NewAnnounceMsg(s)) r.Send(message.NewAnnounceMsg(s))
return nil return nil
} }
// 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 *User) (*Member, bool) { func (r *Room) Member(u *message.User) (*Member, bool) {
m, ok := r.MemberById(u.Id()) m, ok := r.MemberById(u.Id())
if !ok { if !ok {
return nil, false return nil, false
@ -195,7 +197,7 @@ func (r *Room) MemberById(id string) (*Member, bool) {
} }
// 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 *User) bool { func (r *Room) IsOp(u *message.User) bool {
m, ok := r.Member(u) m, ok := r.Member(u)
return ok && m.Op return ok && m.Op
} }

View File

@ -3,11 +3,29 @@ package chat
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/shazow/ssh-chat/chat/message"
) )
// Used for testing
type MockScreen struct {
buffer []byte
}
func (s *MockScreen) Write(data []byte) (n int, err error) {
s.buffer = append(s.buffer, data...)
return len(data), nil
}
func (s *MockScreen) Read(p *[]byte) (n int, err error) {
*p = s.buffer
s.buffer = []byte{}
return len(*p), nil
}
func TestRoomServe(t *testing.T) { func TestRoomServe(t *testing.T) {
ch := NewRoom() ch := NewRoom()
ch.Send(NewAnnounceMsg("hello")) ch.Send(message.NewAnnounceMsg("hello"))
received := <-ch.broadcast received := <-ch.broadcast
actual := received.String() actual := received.String()
@ -22,7 +40,7 @@ func TestRoomJoin(t *testing.T) {
var expected, actual []byte var expected, actual []byte
s := &MockScreen{} s := &MockScreen{}
u := NewUser(testId("foo")) u := message.NewUser(message.SimpleId("foo"))
ch := NewRoom() ch := NewRoom()
go ch.Serve() go ch.Serve()
@ -33,24 +51,24 @@ func TestRoomJoin(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
u.ConsumeOne(s) u.HandleMsg(<-u.ConsumeChan(), s)
expected = []byte(" * foo joined. (Connected: 1)" + Newline) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline)
s.Read(&actual) s.Read(&actual)
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
} }
ch.Send(NewSystemMsg("hello", u)) ch.Send(message.NewSystemMsg("hello", u))
u.ConsumeOne(s) u.HandleMsg(<-u.ConsumeChan(), s)
expected = []byte("-> hello" + Newline) expected = []byte("-> hello" + message.Newline)
s.Read(&actual) s.Read(&actual)
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
} }
ch.Send(ParseInput("/me says hello.", u)) ch.Send(message.ParseInput("/me says hello.", u))
u.ConsumeOne(s) u.HandleMsg(<-u.ConsumeChan(), s)
expected = []byte("** foo says hello." + Newline) expected = []byte("** foo says hello." + message.Newline)
s.Read(&actual) s.Read(&actual)
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
@ -58,8 +76,8 @@ func TestRoomJoin(t *testing.T) {
} }
func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
u := NewUser(testId("foo")) u := message.NewUser(message.SimpleId("foo"))
u.Config = UserConfig{ u.Config = message.UserConfig{
Quiet: true, Quiet: true,
} }
@ -75,8 +93,8 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
<-ch.broadcast <-ch.broadcast
go func() { go func() {
for msg := range u.msg { for msg := range u.ConsumeChan() {
if _, ok := msg.(*AnnounceMsg); ok { if _, ok := msg.(*message.AnnounceMsg); ok {
t.Errorf("Got unexpected `%T`", msg) t.Errorf("Got unexpected `%T`", msg)
} }
} }
@ -84,17 +102,17 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
// Call with an AnnounceMsg and all the other types // Call with an AnnounceMsg and all the other types
// and assert we received only non-announce messages // and assert we received only non-announce messages
ch.HandleMsg(NewAnnounceMsg("Ignored")) ch.HandleMsg(message.NewAnnounceMsg("Ignored"))
// Assert we still get all other types of messages // Assert we still get all other types of messages
ch.HandleMsg(NewEmoteMsg("hello", u)) ch.HandleMsg(message.NewEmoteMsg("hello", u))
ch.HandleMsg(NewSystemMsg("hello", u)) ch.HandleMsg(message.NewSystemMsg("hello", u))
ch.HandleMsg(NewPrivateMsg("hello", u, u)) ch.HandleMsg(message.NewPrivateMsg("hello", u, u))
ch.HandleMsg(NewPublicMsg("hello", u)) ch.HandleMsg(message.NewPublicMsg("hello", u))
} }
func TestRoomQuietToggleBroadcasts(t *testing.T) { func TestRoomQuietToggleBroadcasts(t *testing.T) {
u := NewUser(testId("foo")) u := message.NewUser(message.SimpleId("foo"))
u.Config = UserConfig{ u.Config = message.UserConfig{
Quiet: true, Quiet: true,
} }
@ -111,19 +129,19 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) {
u.ToggleQuietMode() u.ToggleQuietMode()
expectedMsg := NewAnnounceMsg("Ignored") expectedMsg := message.NewAnnounceMsg("Ignored")
ch.HandleMsg(expectedMsg) ch.HandleMsg(expectedMsg)
msg := <-u.msg msg := <-u.ConsumeChan()
if _, ok := msg.(*AnnounceMsg); !ok { if _, ok := msg.(*message.AnnounceMsg); !ok {
t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg) t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg)
} }
u.ToggleQuietMode() u.ToggleQuietMode()
ch.HandleMsg(NewAnnounceMsg("Ignored")) ch.HandleMsg(message.NewAnnounceMsg("Ignored"))
ch.HandleMsg(NewSystemMsg("hello", u)) ch.HandleMsg(message.NewSystemMsg("hello", u))
msg = <-u.msg msg = <-u.ConsumeChan()
if _, ok := msg.(*AnnounceMsg); ok { if _, ok := msg.(*message.AnnounceMsg); ok {
t.Errorf("Got unexpected `%T`", msg) t.Errorf("Got unexpected `%T`", msg)
} }
} }
@ -132,7 +150,7 @@ func TestQuietToggleDisplayState(t *testing.T) {
var expected, actual []byte var expected, actual []byte
s := &MockScreen{} s := &MockScreen{}
u := NewUser(testId("foo")) u := message.NewUser(message.SimpleId("foo"))
ch := NewRoom() ch := NewRoom()
go ch.Serve() go ch.Serve()
@ -146,17 +164,17 @@ func TestQuietToggleDisplayState(t *testing.T) {
// Drain the initial Join message // Drain the initial Join message
<-ch.broadcast <-ch.broadcast
ch.Send(ParseInput("/quiet", u)) ch.Send(message.ParseInput("/quiet", u))
u.ConsumeOne(s) u.HandleMsg(<-u.ConsumeChan(), s)
expected = []byte("-> Quiet mode is toggled ON" + Newline) expected = []byte("-> Quiet mode is toggled ON" + message.Newline)
s.Read(&actual) s.Read(&actual)
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
} }
ch.Send(ParseInput("/quiet", u)) ch.Send(message.ParseInput("/quiet", u))
u.ConsumeOne(s) u.HandleMsg(<-u.ConsumeChan(), s)
expected = []byte("-> Quiet mode is toggled OFF" + Newline) expected = []byte("-> Quiet mode is toggled OFF" + message.Newline)
s.Read(&actual) s.Read(&actual)
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
@ -168,7 +186,7 @@ func TestRoomNames(t *testing.T) {
var expected, actual []byte var expected, actual []byte
s := &MockScreen{} s := &MockScreen{}
u := NewUser(testId("foo")) u := message.NewUser(message.SimpleId("foo"))
ch := NewRoom() ch := NewRoom()
go ch.Serve() go ch.Serve()
@ -182,9 +200,9 @@ func TestRoomNames(t *testing.T) {
// Drain the initial Join message // Drain the initial Join message
<-ch.broadcast <-ch.broadcast
ch.Send(ParseInput("/names", u)) ch.Send(message.ParseInput("/names", u))
u.ConsumeOne(s) u.HandleMsg(<-u.ConsumeChan(), s)
expected = []byte("-> 1 connected: foo" + Newline) expected = []byte("-> 1 connected: foo" + message.Newline)
s.Read(&actual) s.Read(&actual)
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)

View File

@ -12,17 +12,22 @@ var ErrIdTaken = errors.New("id already taken")
// The error returned when a requested item does not exist in the set. // The error returned when a requested item does not exist in the set.
var ErrItemMissing = errors.New("item does not exist") var ErrItemMissing = errors.New("item does not exist")
// Interface for an item storeable in the set
type Item interface {
Id() string
}
// Set with string lookup. // Set with string lookup.
// TODO: Add trie for efficient prefix lookup? // TODO: Add trie for efficient prefix lookup?
type Set struct { type Set struct {
lookup map[string]Identifier lookup map[string]Item
sync.RWMutex sync.RWMutex
} }
// NewSet creates a new set. // NewSet creates a new set.
func NewSet() *Set { func NewSet() *Set {
return &Set{ return &Set{
lookup: map[string]Identifier{}, lookup: map[string]Item{},
} }
} }
@ -30,7 +35,7 @@ func NewSet() *Set {
func (s *Set) Clear() int { func (s *Set) Clear() int {
s.Lock() s.Lock()
n := len(s.lookup) n := len(s.lookup)
s.lookup = map[string]Identifier{} s.lookup = map[string]Item{}
s.Unlock() s.Unlock()
return n return n
} }
@ -41,7 +46,7 @@ func (s *Set) Len() int {
} }
// In checks if an item exists in this set. // In checks if an item exists in this set.
func (s *Set) In(item Identifier) bool { func (s *Set) In(item Item) bool {
s.RLock() s.RLock()
_, ok := s.lookup[item.Id()] _, ok := s.lookup[item.Id()]
s.RUnlock() s.RUnlock()
@ -49,7 +54,7 @@ func (s *Set) In(item Identifier) bool {
} }
// Get returns an item with the given Id. // Get returns an item with the given Id.
func (s *Set) Get(id string) (Identifier, error) { func (s *Set) Get(id string) (Item, error) {
s.RLock() s.RLock()
item, ok := s.lookup[id] item, ok := s.lookup[id]
s.RUnlock() s.RUnlock()
@ -62,7 +67,7 @@ func (s *Set) Get(id string) (Identifier, error) {
} }
// 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 Identifier) error { func (s *Set) Add(item Item) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@ -76,7 +81,7 @@ func (s *Set) Add(item Identifier) error {
} }
// Remove item from this set. // Remove item from this set.
func (s *Set) Remove(item Identifier) error { func (s *Set) Remove(item Item) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
id := item.Id() id := item.Id()
@ -88,9 +93,9 @@ func (s *Set) Remove(item Identifier) error {
return nil return nil
} }
// Replace item from old id with new Identifier. // Replace item from old id with new Item.
// Used for moving the same identifier to a new Id, such as a rename. // Used for moving the same Item to a new Id, such as a rename.
func (s *Set) Replace(oldId string, item Identifier) error { func (s *Set) Replace(oldId string, item Item) error {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@ -107,7 +112,7 @@ func (s *Set) Replace(oldId string, item Identifier) error {
} }
delete(s.lookup, oldId) delete(s.lookup, oldId)
// Add new identifier // Add new Item
s.lookup[item.Id()] = item s.lookup[item.Id()] = item
return nil return nil
@ -115,7 +120,7 @@ func (s *Set) Replace(oldId string, item Identifier) error {
// Each loops over every item while holding a read lock and applies fn to each // Each loops over every item while holding a read lock and applies fn to each
// element. // element.
func (s *Set) Each(fn func(item Identifier)) { func (s *Set) Each(fn func(item Item)) {
s.RLock() s.RLock()
for _, item := range s.lookup { for _, item := range s.lookup {
fn(item) fn(item)
@ -124,8 +129,8 @@ func (s *Set) Each(fn func(item Identifier)) {
} }
// ListPrefix returns a list of items with a prefix, case insensitive. // ListPrefix returns a list of items with a prefix, case insensitive.
func (s *Set) ListPrefix(prefix string) []Identifier { func (s *Set) ListPrefix(prefix string) []Item {
r := []Identifier{} r := []Item{}
prefix = strings.ToLower(prefix) prefix = strings.ToLower(prefix)
s.RLock() s.RLock()

View File

@ -1,11 +1,15 @@
package chat package chat
import "testing" import (
"testing"
"github.com/shazow/ssh-chat/chat/message"
)
func TestSet(t *testing.T) { func TestSet(t *testing.T) {
var err error var err error
s := NewSet() s := NewSet()
u := NewUser(testId("foo")) u := message.NewUser(message.SimpleId("foo"))
if s.In(u) { if s.In(u) {
t.Errorf("Set should be empty.") t.Errorf("Set should be empty.")
@ -20,7 +24,7 @@ func TestSet(t *testing.T) {
t.Errorf("Set should contain user.") t.Errorf("Set should contain user.")
} }
u2 := NewUser(testId("bar")) u2 := message.NewUser(message.SimpleId("bar"))
err = s.Add(u2) err = s.Add(u2)
if err != nil { if err != nil {
t.Error(err) t.Error(err)

3
cmd.go
View File

@ -16,6 +16,7 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
import _ "net/http/pprof" import _ "net/http/pprof"
@ -109,7 +110,7 @@ func main() {
host := NewHost(s) host := NewHost(s)
host.auth = auth host.auth = auth
host.theme = &chat.Themes[0] host.theme = &message.Themes[0]
err = fromFile(options.Admin, func(line []byte) error { err = fromFile(options.Admin, func(line []byte) error {
key, _, _, _, err := ssh.ParseAuthorizedKey(line) key, _, _, _, err := ssh.ParseAuthorizedKey(line)

59
host.go
View File

@ -9,13 +9,14 @@ import (
"github.com/shazow/rateio" "github.com/shazow/rateio"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
const maxInputLength int = 1024 const maxInputLength int = 1024
// GetPrompt will render the terminal prompt string based on the user. // GetPrompt will render the terminal prompt string based on the user.
func GetPrompt(user *chat.User) string { func GetPrompt(user *message.User) string {
name := user.Name() name := user.Name()
if user.Config.Theme != nil { if user.Config.Theme != nil {
name = user.Config.Theme.ColorName(user) name = user.Config.Theme.ColorName(user)
@ -35,7 +36,7 @@ type Host struct {
count int count int
// Default theme // Default theme
theme *chat.Theme theme *message.Theme
} }
// NewHost creates a Host on top of an existing listener. // NewHost creates a Host on top of an existing listener.
@ -72,7 +73,7 @@ func (h Host) isOp(conn sshd.Connection) bool {
// 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) id := NewIdentity(term.Conn)
user := chat.NewUserScreen(id, term) user := message.NewUserScreen(id, term)
user.Config.Theme = h.theme user.Config.Theme = h.theme
go func() { go func() {
// Close term once user is closed. // Close term once user is closed.
@ -83,7 +84,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
// Send MOTD // Send MOTD
if h.motd != "" { if h.motd != "" {
user.Send(chat.NewAnnounceMsg(h.motd)) user.Send(message.NewAnnounceMsg(h.motd))
} }
member, err := h.Join(user) member, err := h.Join(user)
@ -119,11 +120,11 @@ func (h *Host) Connect(term *sshd.Terminal) {
err = ratelimit.Count(1) err = ratelimit.Count(1)
if err != nil { if err != nil {
user.Send(chat.NewSystemMsg("Message rejected: Rate limiting is in effect.", user)) user.Send(message.NewSystemMsg("Message rejected: Rate limiting is in effect.", user))
continue continue
} }
if len(line) > maxInputLength { if len(line) > maxInputLength {
user.Send(chat.NewSystemMsg("Message rejected: Input too long.", user)) user.Send(message.NewSystemMsg("Message rejected: Input too long.", user))
continue continue
} }
if line == "" { if line == "" {
@ -131,7 +132,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
continue continue
} }
m := chat.ParseInput(line, user) m := message.ParseInput(line, user)
// FIXME: Any reason to use h.room.Send(m) instead? // FIXME: Any reason to use h.room.Send(m) instead?
h.HandleMsg(m) h.HandleMsg(m)
@ -184,7 +185,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 *chat.User) func(line string, pos int, key rune) (newLine string, newPos int, ok bool) { func (h *Host) AutoCompleteFunction(u *message.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
@ -231,8 +232,8 @@ func (h *Host) AutoCompleteFunction(u *chat.User) func(line string, pos int, key
} }
} }
// GetUser returns a chat.User based on a name. // GetUser returns a message.User based on a name.
func (h *Host) GetUser(name string) (*chat.User, bool) { func (h *Host) GetUser(name string) (*message.User, bool) {
m, ok := h.MemberById(name) m, ok := h.MemberById(name)
if !ok { if !ok {
return nil, false return nil, false
@ -247,7 +248,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/msg", Prefix: "/msg",
PrefixHelp: "USER MESSAGE", PrefixHelp: "USER MESSAGE",
Help: "Send MESSAGE to USER.", Help: "Send MESSAGE to USER.",
Handler: func(room *chat.Room, msg chat.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
args := msg.Args() args := msg.Args()
switch len(args) { switch len(args) {
case 0: case 0:
@ -261,7 +262,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("user not found") return errors.New("user not found")
} }
m := chat.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target) m := message.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target)
room.Send(m) room.Send(m)
return nil return nil
}, },
@ -271,7 +272,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/reply", Prefix: "/reply",
PrefixHelp: "MESSAGE", PrefixHelp: "MESSAGE",
Help: "Reply with MESSAGE to the previous private message.", Help: "Reply with MESSAGE to the previous private message.",
Handler: func(room *chat.Room, msg chat.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
args := msg.Args() args := msg.Args()
switch len(args) { switch len(args) {
case 0: case 0:
@ -283,7 +284,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("no message to reply to") return errors.New("no message to reply to")
} }
m := chat.NewPrivateMsg(strings.Join(args, " "), msg.From(), target) m := message.NewPrivateMsg(strings.Join(args, " "), msg.From(), target)
room.Send(m) room.Send(m)
return nil return nil
}, },
@ -293,7 +294,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/whois", Prefix: "/whois",
PrefixHelp: "USER", PrefixHelp: "USER",
Help: "Information about USER.", Help: "Information about USER.",
Handler: func(room *chat.Room, msg chat.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
args := msg.Args() args := msg.Args()
if len(args) == 0 { if len(args) == 0 {
return errors.New("must specify user") return errors.New("must specify user")
@ -305,7 +306,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
} }
id := target.Identifier.(*Identity) id := target.Identifier.(*Identity)
room.Send(chat.NewSystemMsg(id.Whois(), msg.From())) room.Send(message.NewSystemMsg(id.Whois(), msg.From()))
return nil return nil
}, },
@ -314,8 +315,8 @@ func (h *Host) InitCommands(c *chat.Commands) {
// Hidden commands // Hidden commands
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/version", Prefix: "/version",
Handler: func(room *chat.Room, msg chat.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
room.Send(chat.NewSystemMsg(buildCommit, msg.From())) room.Send(message.NewSystemMsg(buildCommit, msg.From()))
return nil return nil
}, },
}) })
@ -323,8 +324,8 @@ func (h *Host) InitCommands(c *chat.Commands) {
timeStarted := time.Now() timeStarted := time.Now()
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/uptime", Prefix: "/uptime",
Handler: func(room *chat.Room, msg chat.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
room.Send(chat.NewSystemMsg(time.Now().Sub(timeStarted).String(), msg.From())) room.Send(message.NewSystemMsg(time.Now().Sub(timeStarted).String(), msg.From()))
return nil return nil
}, },
}) })
@ -335,7 +336,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/kick", Prefix: "/kick",
PrefixHelp: "USER", PrefixHelp: "USER",
Help: "Kick USER from the server.", Help: "Kick USER from the server.",
Handler: func(room *chat.Room, msg chat.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")
} }
@ -351,7 +352,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
} }
body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name()) body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name())
room.Send(chat.NewAnnounceMsg(body)) room.Send(message.NewAnnounceMsg(body))
target.Close() target.Close()
return nil return nil
}, },
@ -362,7 +363,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/ban", Prefix: "/ban",
PrefixHelp: "USER [DURATION]", PrefixHelp: "USER [DURATION]",
Help: "Ban USER from the server.", Help: "Ban USER from the server.",
Handler: func(room *chat.Room, msg chat.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
// TODO: Would be nice to specify what to ban. Key? Ip? etc. // TODO: Would be nice to specify what to ban. Key? Ip? etc.
if !room.IsOp(msg.From()) { if !room.IsOp(msg.From()) {
return errors.New("must be op") return errors.New("must be op")
@ -388,7 +389,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
h.auth.BanAddr(id.RemoteAddr(), until) h.auth.BanAddr(id.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(chat.NewAnnounceMsg(body)) room.Send(message.NewAnnounceMsg(body))
target.Close() target.Close()
logger.Debugf("Banned: \n-> %s", id.Whois()) logger.Debugf("Banned: \n-> %s", id.Whois())
@ -402,7 +403,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/motd", Prefix: "/motd",
PrefixHelp: "MESSAGE", PrefixHelp: "MESSAGE",
Help: "Set the MESSAGE of the day.", Help: "Set the MESSAGE of the day.",
Handler: func(room *chat.Room, msg chat.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")
} }
@ -415,9 +416,9 @@ func (h *Host) InitCommands(c *chat.Commands) {
h.motd = motd h.motd = motd
body := fmt.Sprintf("New message of the day set by %s:", msg.From().Name()) body := fmt.Sprintf("New message of the day set by %s:", msg.From().Name())
room.Send(chat.NewAnnounceMsg(body)) room.Send(message.NewAnnounceMsg(body))
if motd != "" { if motd != "" {
room.Send(chat.NewAnnounceMsg(motd)) room.Send(message.NewAnnounceMsg(motd))
} }
return nil return nil
@ -429,7 +430,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
Prefix: "/op", Prefix: "/op",
PrefixHelp: "USER [DURATION]", PrefixHelp: "USER [DURATION]",
Help: "Set USER as admin.", Help: "Set USER as admin.",
Handler: func(room *chat.Room, msg chat.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")
} }
@ -453,7 +454,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
h.auth.Op(id.PublicKey(), until) h.auth.Op(id.PublicKey(), until)
body := fmt.Sprintf("Made op by %s.", msg.From().Name()) body := fmt.Sprintf("Made op by %s.", msg.From().Name())
room.Send(chat.NewSystemMsg(body, member.User)) room.Send(message.NewSystemMsg(body, member.User))
return nil return nil
}, },

View File

@ -10,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@ -26,7 +26,7 @@ func stripPrompt(s string) string {
func TestHostGetPrompt(t *testing.T) { func TestHostGetPrompt(t *testing.T) {
var expected, actual string var expected, actual string
u := chat.NewUser(&Identity{nil, "foo"}) u := message.NewUser(&Identity{nil, "foo"})
u.SetColorIdx(2) u.SetColorIdx(2)
actual = GetPrompt(u) actual = GetPrompt(u)
@ -35,7 +35,7 @@ func TestHostGetPrompt(t *testing.T) {
t.Errorf("Got: %q; Expected: %q", actual, expected) t.Errorf("Got: %q; Expected: %q", actual, expected)
} }
u.Config.Theme = &chat.Themes[0] u.Config.Theme = &message.Themes[0]
actual = GetPrompt(u) actual = GetPrompt(u)
expected = "[\033[38;05;2mfoo\033[0m] " expected = "[\033[38;05;2mfoo\033[0m] "
if actual != expected { if actual != expected {

View File

@ -5,6 +5,7 @@ import (
"net" "net"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
@ -44,7 +45,7 @@ func (i Identity) Whois() string {
if i.PublicKey() != nil { if i.PublicKey() != nil {
fingerprint = sshd.Fingerprint(i.PublicKey()) fingerprint = sshd.Fingerprint(i.PublicKey())
} }
return fmt.Sprintf("name: %s"+chat.Newline+ return fmt.Sprintf("name: %s"+message.Newline+
" > ip: %s"+chat.Newline+ " > ip: %s"+message.Newline+
" > fingerprint: %s", i.Name(), ip, fingerprint) " > fingerprint: %s", i.Name(), ip, fingerprint)
} }