From bccb5f3c322398a95e875a0c1d4f663a2db1efaa Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 29 Aug 2016 13:15:03 -0400 Subject: [PATCH 01/21] refactor: User.Ignored, message.User; add: set.Keyize --- chat/command.go | 25 ++++++++++++++++++------- chat/member.go | 24 ++++++++++++++++++++++++ chat/message/user.go | 7 +++---- chat/room.go | 35 +++++++++++++++-------------------- chat/room_test.go | 4 ++-- host.go | 14 +++++++------- host_test.go | 3 ++- set/item.go | 4 ++++ 8 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 chat/member.go diff --git a/chat/command.go b/chat/command.go index df403dc2..76e01a7b 100644 --- a/chat/command.go +++ b/chat/command.go @@ -24,6 +24,10 @@ var ErrMissingArg = errors.New("missing argument") // The error returned when a command is added without a 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. type Command struct { // The command's key, such as /foo @@ -146,11 +150,9 @@ func InitCommands(c *Commands) { if len(args) != 1 { return ErrMissingArg } - u := msg.From() - - member, ok := room.MemberByID(u.ID()) + member, ok := room.MemberByID(msg.From().ID()) if !ok { - return errors.New("failed to find member") + return ErrMissingMember } oldID := member.ID() @@ -251,11 +253,16 @@ func InitCommands(c *Commands) { PrefixHelp: "[USER]", Help: "Hide messages from USER, /unignore USER to stop hiding.", 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")) if id == "" { // Print ignored names, if any. 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()) return nil }) @@ -279,7 +286,7 @@ func InitCommands(c *Commands) { 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 { return fmt.Errorf("user already ignored: %s", id) } else if err != nil { @@ -295,12 +302,16 @@ func InitCommands(c *Commands) { Prefix: "/unignore", PrefixHelp: "USER", 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")) if id == "" { 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 } diff --git a/chat/member.go b/chat/member.go new file mode 100644 index 00000000..cc379419 --- /dev/null +++ b/chat/member.go @@ -0,0 +1,24 @@ +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 { + ID() string + SetID(string) + + Name() string + + Config() message.UserConfig + SetConfig(message.UserConfig) + + Send(message.Message) error +} diff --git a/chat/message/user.go b/chat/message/user.go index 0cd700c3..dba68cf9 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -8,8 +8,6 @@ import ( "regexp" "sync" "time" - - "github.com/shazow/ssh-chat/set" ) const messageBuffer = 5 @@ -21,7 +19,6 @@ var ErrUserClosed = errors.New("user closed") // User definition, implemented set Item interface and io.Writer type User struct { Identifier - Ignored *set.Set colorIdx int joined time.Time msg chan Message @@ -42,7 +39,6 @@ func NewUser(identity Identifier) *User { joined: time.Now(), msg: make(chan Message, messageBuffer), done: make(chan struct{}), - Ignored: set.New(), } u.setColorIdx(rand.Int()) @@ -83,6 +79,7 @@ func (u *User) ReplyTo() *User { // SetReplyTo sets the last user to message this user. func (u *User) SetReplyTo(user *User) { + // TODO: Use UserConfig.ReplyTo string u.mu.Lock() defer u.mu.Unlock() u.replyTo = user @@ -122,11 +119,13 @@ func (u *User) Consume() { } // Consume one message and stop, mostly for testing +// TODO: Stop using it and remove it. func (u *User) ConsumeOne() Message { return <-u.msg } // Check if there are pending messages, used for testing +// TODO: Stop using it and remove it. func (u *User) HasMessages() bool { select { case msg := <-u.msg: diff --git a/chat/room.go b/chat/room.go index 7ce6de1e..fdc118d8 100644 --- a/chat/room.go +++ b/chat/room.go @@ -21,11 +21,6 @@ var ErrRoomClosed = errors.New("room closed") // as empty string. 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 type Room struct { topic string @@ -58,14 +53,10 @@ func (r *Room) SetCommands(commands Commands) { r.commands = commands } -// Close the room and all the users it contains. +// Close the room func (r *Room) Close() { r.closeOnce.Do(func() { r.closed = true - r.Members.Each(func(_ string, item set.Item) error { - item.Value().(*Member).Close() - return nil - }) r.Members.Clear() close(r.broadcast) }) @@ -98,9 +89,10 @@ func (r *Room) HandleMsg(m message.Message) { r.history.Add(m) r.Members.Each(func(_ 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 && roomMember.Ignored.In(fromMsg.From().ID()) { // Skip because ignored return } @@ -142,12 +134,15 @@ func (r *Room) History(u *message.User) { } // Join the room as a user, will announce. -func (r *Room) Join(u *message.User) (*Member, error) { +func (r *Room) Join(u *message.User) (*roomMember, error) { // TODO: Check if closed if u.ID() == "" { return nil, ErrInvalidName } - member := &Member{u} + member := &roomMember{ + Member: u, + Ignored: set.New(), + } err := r.Members.Add(set.Itemize(u.ID(), member)) if err != nil { return nil, err @@ -187,28 +182,28 @@ func (r *Room) Rename(oldID string, u message.Identifier) error { // Member returns a corresponding Member object to a User if the Member is // present in this room. -func (r *Room) Member(u *message.User) (*Member, bool) { +func (r *Room) Member(u message.Identifier) (*roomMember, bool) { m, ok := r.MemberByID(u.ID()) if !ok { return nil, false } // Check that it's the same user - if m.User != u { + if m.Member != u { return nil, false } 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) if err != nil { return nil, false } - return m.Value().(*Member), true + return m.Value().(*roomMember), true } // 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.Identifier) bool { return r.Ops.In(u.ID()) } @@ -228,7 +223,7 @@ func (r *Room) NamesPrefix(prefix string) []string { items := r.Members.ListPrefix(prefix) names := make([]string, len(items)) for i, item := range items { - names[i] = item.Value().(*Member).User.Name() + names[i] = item.Value().(*roomMember).Name() } return names } diff --git a/chat/room_test.go b/chat/room_test.go index e30c170a..5f4b02f3 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -2,10 +2,8 @@ package chat import ( "errors" - "fmt" "reflect" "testing" - "time" "github.com/shazow/ssh-chat/chat/message" ) @@ -48,6 +46,7 @@ type ScreenedUser struct { screen *MockScreen } +/* func TestIgnore(t *testing.T) { var buffer []byte @@ -151,6 +150,7 @@ func TestIgnore(t *testing.T) { ignorer.screen.Read(&buffer) expectOutput(t, buffer, ignored.user.Name()+": hello again!"+message.Newline) } +*/ func expectOutput(t *testing.T, buffer []byte, expected string) { bytes := []byte(expected) diff --git a/host.go b/host.go index 752cf2ff..42fb39e7 100644 --- a/host.go +++ b/host.go @@ -130,7 +130,7 @@ func (h *Host) Connect(term *sshd.Terminal) { // Should the user be op'd on join? if h.isOp(term.Conn) { - h.Room.Ops.Add(set.Itemize(member.ID(), member)) + h.Room.Ops.Add(set.Keyize(member.ID())) } ratelimit := rateio.NewSimpleLimiter(3, time.Second*3) @@ -273,7 +273,8 @@ func (h *Host) GetUser(name string) (*message.User, bool) { if !ok { return nil, false } - return m.User, true + u, ok := m.Member.(*message.User) + return u, ok } // InitCommands adds host-specific commands to a Commands container. These will @@ -505,17 +506,16 @@ func (h *Host) InitCommands(c *chat.Commands) { until, _ = time.ParseDuration(args[1]) } - member, ok := room.MemberByID(args[0]) + user, ok := h.GetUser(args[0]) if !ok { return errors.New("user not found") } - room.Ops.Add(set.Itemize(member.ID(), member)) + room.Ops.Add(set.Keyize(user.ID())) - id := member.Identifier.(*Identity) - h.auth.Op(id.PublicKey(), until) + h.auth.Op(user.Identifier.(*Identity).PublicKey(), until) body := fmt.Sprintf("Made op by %s.", msg.From().Name()) - room.Send(message.NewSystemMsg(body, member.User)) + room.Send(message.NewSystemMsg(body, user)) return nil }, diff --git a/host_test.go b/host_test.go index 7c2f9764..7a42b8fa 100644 --- a/host_test.go +++ b/host_test.go @@ -115,6 +115,7 @@ func TestHostNameCollision(t *testing.T) { actual := scanner.Text() if !strings.HasPrefix(actual, "[Guest1] ") { + // FIXME: Flaky? t.Errorf("Second client did not get Guest1 name: %q", actual) } return nil @@ -195,7 +196,7 @@ func TestHostKick(t *testing.T) { if member == nil { 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 connected <- struct{}{} diff --git a/set/item.go b/set/item.go index 25edf662..d912cf87 100644 --- a/set/item.go +++ b/set/item.go @@ -25,6 +25,10 @@ func Itemize(key string, value interface{}) Item { return &item{key, value} } +func Keyize(key string) Item { + return &item{key, struct{}{}} +} + type StringItem string func (item StringItem) Key() string { From 4bca1f0294e1fe0b0c7453d5bc9230e91ac3b5bf Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 1 Sep 2016 16:22:21 -0400 Subject: [PATCH 02/21] refactor: Identifier.SetID -> Identifier.SetName Identifier.ID() is now read-only and derived from name. --- .gitignore | 1 + chat/command.go | 4 ++-- chat/member.go | 2 +- chat/message/identity.go | 12 ++++++------ chat/message/user.go | 4 ++-- identity.go | 6 +----- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 593c721c..10fc9fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build +/vendor host_key host_key.pub ssh-chat diff --git a/chat/command.go b/chat/command.go index 76e01a7b..1d8e6fb6 100644 --- a/chat/command.go +++ b/chat/command.go @@ -156,10 +156,10 @@ func InitCommands(c *Commands) { } oldID := member.ID() - member.SetID(SanitizeName(args[0])) + member.SetName(args[0]) err := room.Rename(oldID, member) if err != nil { - member.SetID(oldID) + member.SetName(oldID) return err } return nil diff --git a/chat/member.go b/chat/member.go index cc379419..f0d3b1cb 100644 --- a/chat/member.go +++ b/chat/member.go @@ -13,9 +13,9 @@ type roomMember struct { type Member interface { ID() string - SetID(string) Name() string + SetName(string) Config() message.UserConfig SetConfig(message.UserConfig) diff --git a/chat/message/identity.go b/chat/message/identity.go index 8edef77b..5b3b18c5 100644 --- a/chat/message/identity.go +++ b/chat/message/identity.go @@ -3,8 +3,8 @@ package message // Identifier is an interface that can uniquely identify itself. type Identifier interface { ID() string - SetID(string) Name() string + SetName(string) } // SimpleID is a simple Identifier implementation used for testing. @@ -15,12 +15,12 @@ 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() } + +// SetName is a no-op +func (i SimpleID) SetName(s string) { + // no-op +} diff --git a/chat/message/user.go b/chat/message/user.go index dba68cf9..b56d68b9 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -65,8 +65,8 @@ func (u *User) SetConfig(cfg UserConfig) { } // Rename the user with a new Identifier. -func (u *User) SetID(id string) { - u.Identifier.SetID(id) +func (u *User) SetName(name string) { + u.Identifier.SetName(name) u.setColorIdx(rand.Int()) } diff --git a/identity.go b/identity.go index 50cee7e5..6f38aa87 100644 --- a/identity.go +++ b/identity.go @@ -30,12 +30,8 @@ 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) + i.id = chat.SanitizeName(name) } func (i Identity) Name() string { From a838ee2cadbb3b0919f33d2bc9d1f28ac3415d6d Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 1 Sep 2016 16:35:09 -0400 Subject: [PATCH 03/21] refactor: Remove mentions of message.User from chat. --- chat/room.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/chat/room.go b/chat/room.go index fdc118d8..a7a70c05 100644 --- a/chat/room.go +++ b/chat/room.go @@ -82,7 +82,7 @@ func (r *Room) HandleMsg(m message.Message) { user.Send(m) default: fromMsg, skip := m.(message.MessageFrom) - var skipUser *message.User + var skipUser Member if skip { skipUser = fromMsg.From() } @@ -127,28 +127,28 @@ func (r *Room) Send(m message.Message) { } // History feeds the room's recent message history to the user's handler. -func (r *Room) History(u *message.User) { - for _, m := range r.history.Get(historyLen) { - u.Send(m) +func (r *Room) History(m Member) { + for _, msg := range r.history.Get(historyLen) { + m.Send(msg) } } // Join the room as a user, will announce. -func (r *Room) Join(u *message.User) (*roomMember, error) { +func (r *Room) Join(m Member) (*roomMember, error) { // TODO: Check if closed - if u.ID() == "" { + if m.ID() == "" { return nil, ErrInvalidName } member := &roomMember{ - Member: u, + Member: m, Ignored: set.New(), } - err := r.Members.Add(set.Itemize(u.ID(), member)) + err := r.Members.Add(set.Itemize(m.ID(), member)) if err != nil { return nil, err } - r.History(u) - s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.Members.Len()) + r.History(m) + s := fmt.Sprintf("%s joined. (Connected: %d)", m.Name(), r.Members.Len()) r.Send(message.NewAnnounceMsg(s)) return member, nil } From b45efc0daef975b40270bf65f1b9e6ca730938bd Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 1 Sep 2016 16:42:12 -0400 Subject: [PATCH 04/21] refactor: Privatize sshchat.GetPrompt and sshchat.Identity --- host.go | 20 ++++++++++---------- host_test.go | 6 +++--- identity.go | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/host.go b/host.go index 42fb39e7..028d757d 100644 --- a/host.go +++ b/host.go @@ -18,8 +18,8 @@ import ( const maxInputLength int = 1024 -// GetPrompt will render the terminal prompt string based on the user. -func GetPrompt(user *message.User) string { +// 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 { @@ -90,8 +90,8 @@ func (h *Host) isOp(conn sshd.Connection) bool { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { - id := NewIdentity(term.Conn) - user := message.NewUserScreen(id, term) + ident := toIdentity(term.Conn) + user := message.NewUserScreen(ident, term) cfg := user.Config() cfg.Theme = &h.theme user.SetConfig(cfg) @@ -115,7 +115,7 @@ func (h *Host) Connect(term *sshd.Terminal) { member, err := h.Join(user) if err != nil { // Try again... - id.SetName(fmt.Sprintf("Guest%d", count)) + ident.SetName(fmt.Sprintf("Guest%d", count)) member, err = h.Join(user) } if err != nil { @@ -124,7 +124,7 @@ func (h *Host) Connect(term *sshd.Terminal) { } // Successfully joined. - term.SetPrompt(GetPrompt(user)) + term.SetPrompt(getPrompt(user)) term.AutoCompleteCallback = h.AutoCompleteFunction(user) user.SetHighlight(user.Name()) @@ -172,7 +172,7 @@ func (h *Host) Connect(term *sshd.Terminal) { // // FIXME: This is hacky, how do we improve the API to allow for // this? Chat module shouldn't know about terminals. - term.SetPrompt(GetPrompt(user)) + term.SetPrompt(getPrompt(user)) user.SetHighlight(user.Name()) } } @@ -356,7 +356,7 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("user not found") } - id := target.Identifier.(*Identity) + id := target.Identifier.(*identity) var whois string switch room.IsOp(msg.From()) { case true: @@ -442,7 +442,7 @@ func (h *Host) InitCommands(c *chat.Commands) { until, _ = time.ParseDuration(args[1]) } - id := target.Identifier.(*Identity) + id := target.Identifier.(*identity) h.auth.Ban(id.PublicKey(), until) h.auth.BanAddr(id.RemoteAddr(), until) @@ -512,7 +512,7 @@ func (h *Host) InitCommands(c *chat.Commands) { } room.Ops.Add(set.Keyize(user.ID())) - h.auth.Op(user.Identifier.(*Identity).PublicKey(), until) + h.auth.Op(user.Identifier.(*identity).PublicKey(), until) body := fmt.Sprintf("Made op by %s.", msg.From().Name()) room.Send(message.NewSystemMsg(body, user)) diff --git a/host_test.go b/host_test.go index 7a42b8fa..b0ec48af 100644 --- a/host_test.go +++ b/host_test.go @@ -27,9 +27,9 @@ func stripPrompt(s string) string { func TestHostGetPrompt(t *testing.T) { var expected, actual string - u := message.NewUser(&Identity{id: "foo"}) + u := message.NewUser(&identity{id: "foo"}) - actual = GetPrompt(u) + actual = getPrompt(u) expected = "[foo] " if actual != expected { t.Errorf("Got: %q; Expected: %q", actual, expected) @@ -38,7 +38,7 @@ func TestHostGetPrompt(t *testing.T) { u.SetConfig(message.UserConfig{ Theme: &message.Themes[0], }) - actual = GetPrompt(u) + actual = getPrompt(u) expected = "[\033[38;05;88mfoo\033[0m] " if actual != expected { t.Errorf("Got: %q; Expected: %q", actual, expected) diff --git a/identity.go b/identity.go index 6f38aa87..b33cb42f 100644 --- a/identity.go +++ b/identity.go @@ -11,35 +11,35 @@ import ( ) // Identity is a container for everything that identifies a client. -type Identity struct { +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{ +// Converts an sshd.Connection to an identity. +func toIdentity(conn sshd.Connection) *identity { + return &identity{ Connection: conn, id: chat.SanitizeName(conn.Name()), created: time.Now(), } } -func (i Identity) ID() string { +func (i identity) ID() string { return i.id } -func (i *Identity) SetName(name string) { +func (i *identity) SetName(name string) { i.id = chat.SanitizeName(name) } -func (i Identity) Name() string { +func (i identity) Name() string { return i.id } // Whois returns a whois description for non-admin users. -func (i Identity) Whois() string { +func (i identity) Whois() string { fingerprint := "(no public key)" if i.PublicKey() != nil { fingerprint = sshd.Fingerprint(i.PublicKey()) @@ -51,7 +51,7 @@ func (i Identity) Whois() string { } // WhoisAdmin returns a whois description for admin users. -func (i Identity) WhoisAdmin() string { +func (i identity) WhoisAdmin() string { ip, _, _ := net.SplitHostPort(i.RemoteAddr().String()) fingerprint := "(no public key)" if i.PublicKey() != nil { From f64b2a3607c0671e53474e785d89dd5624213d50 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 1 Sep 2016 16:53:29 -0400 Subject: [PATCH 05/21] refactor: sshchat.getPrompt() -> message.User.Prompt() --- chat/message/user.go | 10 ++++++++++ host.go | 14 ++------------ host_test.go | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/chat/message/user.go b/chat/message/user.go index b56d68b9..46bfb52a 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -186,6 +186,16 @@ func (u *User) Send(m Message) error { return nil } +// Prompt renders a theme-colorized prompt string. +func (u *User) Prompt() string { + name := u.Name() + cfg := u.Config() + if cfg.Theme != nil { + name = cfg.Theme.ColorName(u) + } + return fmt.Sprintf("[%s] ", name) +} + // Container for per-user configurations. type UserConfig struct { Highlight *regexp.Regexp diff --git a/host.go b/host.go index 028d757d..ca3425d7 100644 --- a/host.go +++ b/host.go @@ -18,16 +18,6 @@ import ( 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 // TODO: Should be easy to add support for multiple rooms, if we want. type Host struct { @@ -124,7 +114,7 @@ func (h *Host) Connect(term *sshd.Terminal) { } // Successfully joined. - term.SetPrompt(getPrompt(user)) + term.SetPrompt(user.Prompt()) term.AutoCompleteCallback = h.AutoCompleteFunction(user) user.SetHighlight(user.Name()) @@ -172,7 +162,7 @@ func (h *Host) Connect(term *sshd.Terminal) { // // FIXME: This is hacky, how do we improve the API to allow for // this? Chat module shouldn't know about terminals. - term.SetPrompt(getPrompt(user)) + term.SetPrompt(user.Prompt()) user.SetHighlight(user.Name()) } } diff --git a/host_test.go b/host_test.go index b0ec48af..405792e2 100644 --- a/host_test.go +++ b/host_test.go @@ -29,7 +29,7 @@ func TestHostGetPrompt(t *testing.T) { u := message.NewUser(&identity{id: "foo"}) - actual = getPrompt(u) + actual = u.Prompt() expected = "[foo] " if actual != expected { t.Errorf("Got: %q; Expected: %q", actual, expected) @@ -38,7 +38,7 @@ func TestHostGetPrompt(t *testing.T) { u.SetConfig(message.UserConfig{ Theme: &message.Themes[0], }) - actual = getPrompt(u) + actual = u.Prompt() expected = "[\033[38;05;88mfoo\033[0m] " if actual != expected { t.Errorf("Got: %q; Expected: %q", actual, expected) From 91718a511b7e977a5bcf347c1cad563de7a788fb Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 4 Sep 2016 16:40:51 -0400 Subject: [PATCH 06/21] progress: refactor: Remove Identifier interface, reverse ownership of sshchat identity and message.User --- chat/message/identity.go | 26 ---- chat/message/message_test.go | 2 +- chat/{ => message}/sanitize.go | 10 +- chat/message/theme_test.go | 2 +- chat/message/user.go | 38 ++++-- chat/message/user_test.go | 2 +- chat/room.go | 8 +- chat/room_test.go | 12 +- chat/set_test.go | 4 +- client.go | 45 +++++++ host.go | 213 +++++++++++++++++---------------- host_test.go | 2 +- identity.go | 65 ---------- sanitize.go | 10 ++ 14 files changed, 211 insertions(+), 228 deletions(-) delete mode 100644 chat/message/identity.go rename chat/{ => message}/sanitize.go (59%) create mode 100644 client.go delete mode 100644 identity.go create mode 100644 sanitize.go diff --git a/chat/message/identity.go b/chat/message/identity.go deleted file mode 100644 index 5b3b18c5..00000000 --- a/chat/message/identity.go +++ /dev/null @@ -1,26 +0,0 @@ -package message - -// Identifier is an interface that can uniquely identify itself. -type Identifier interface { - ID() string - Name() string - SetName(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) -} - -// Name returns the ID -func (i SimpleID) Name() string { - return i.ID() -} - -// SetName is a no-op -func (i SimpleID) SetName(s string) { - // no-op -} diff --git a/chat/message/message_test.go b/chat/message/message_test.go index d7ef2854..cd0e3a38 100644 --- a/chat/message/message_test.go +++ b/chat/message/message_test.go @@ -11,7 +11,7 @@ func TestMessage(t *testing.T) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } - u := NewUser(SimpleID("foo")) + u := NewUser("foo") expected = "foo: hello" actual = NewPublicMsg("hello", u).String() if actual != expected { diff --git a/chat/sanitize.go b/chat/message/sanitize.go similarity index 59% rename from chat/sanitize.go rename to chat/message/sanitize.go index b567825d..fc13873c 100644 --- a/chat/sanitize.go +++ b/chat/message/sanitize.go @@ -1,8 +1,9 @@ -package chat +package message import "regexp" var reStripName = regexp.MustCompile("[^\\w.-]") + const maxLength = 16 // SanitizeName returns a name with only allowed characters and a reasonable length @@ -15,10 +16,3 @@ func SanitizeName(s string) string { s = s[:nameLength] 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, "") -} diff --git a/chat/message/theme_test.go b/chat/message/theme_test.go index a62c20ef..d3d45774 100644 --- a/chat/message/theme_test.go +++ b/chat/message/theme_test.go @@ -51,7 +51,7 @@ func TestTheme(t *testing.T) { t.Errorf("Got: %q; Expected: %q", actual, expected) } - u := NewUser(SimpleID("foo")) + u := NewUser("foo") u.colorIdx = 4 actual = colorTheme.ColorName(u) expected = "\033[38;05;5mfoo\033[0m" diff --git a/chat/message/user.go b/chat/message/user.go index 46bfb52a..d10d02b5 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -18,7 +18,6 @@ var ErrUserClosed = errors.New("user closed") // User definition, implemented set Item interface and io.Writer type User struct { - Identifier colorIdx int joined time.Time msg chan Message @@ -28,25 +27,26 @@ type User struct { closeOnce sync.Once mu sync.Mutex + name string config UserConfig replyTo *User // Set when user gets a /msg, for replying. } -func NewUser(identity Identifier) *User { +func NewUser(name string) *User { u := User{ - Identifier: identity, - config: DefaultUserConfig, - joined: time.Now(), - msg: make(chan Message, messageBuffer), - done: make(chan struct{}), + name: name, + config: DefaultUserConfig, + joined: time.Now(), + msg: make(chan Message, messageBuffer), + done: make(chan struct{}), } u.setColorIdx(rand.Int()) return &u } -func NewUserScreen(identity Identifier, screen io.WriteCloser) *User { - u := NewUser(identity) +func NewUserScreen(name string, screen io.WriteCloser) *User { + u := NewUser(name) u.screen = screen return u @@ -64,10 +64,28 @@ func (u *User) SetConfig(cfg UserConfig) { u.mu.Unlock() } +func (u *User) ID() string { + u.mu.Lock() + defer u.mu.Unlock() + return SanitizeName(u.name) +} + +func (u *User) Name() string { + u.mu.Lock() + defer u.mu.Unlock() + return u.name +} + +func (u *User) Joined() time.Time { + return u.joined +} + // Rename the user with a new Identifier. func (u *User) SetName(name string) { - u.Identifier.SetName(name) + u.mu.Lock() + u.name = name u.setColorIdx(rand.Int()) + u.mu.Unlock() } // ReplyTo returns the last user that messaged this user. diff --git a/chat/message/user_test.go b/chat/message/user_test.go index f70e6747..871730d9 100644 --- a/chat/message/user_test.go +++ b/chat/message/user_test.go @@ -9,7 +9,7 @@ func TestMakeUser(t *testing.T) { var actual, expected []byte s := &MockScreen{} - u := NewUserScreen(SimpleID("foo"), s) + u := NewUserScreen("foo", s) m := NewAnnounceMsg("hello") defer u.Close() diff --git a/chat/room.go b/chat/room.go index a7a70c05..1bf85db3 100644 --- a/chat/room.go +++ b/chat/room.go @@ -154,7 +154,7 @@ func (r *Room) Join(m Member) (*roomMember, error) { } // 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()) if err != nil { return err @@ -166,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. -func (r *Room) Rename(oldID string, u message.Identifier) error { +func (r *Room) Rename(oldID string, u Member) error { if u.ID() == "" { return ErrInvalidName } @@ -182,7 +182,7 @@ func (r *Room) Rename(oldID string, u message.Identifier) error { // Member returns a corresponding Member object to a User if the Member is // present in this room. -func (r *Room) Member(u message.Identifier) (*roomMember, bool) { +func (r *Room) Member(u Member) (*roomMember, bool) { m, ok := r.MemberByID(u.ID()) if !ok { return nil, false @@ -203,7 +203,7 @@ func (r *Room) MemberByID(id string) (*roomMember, bool) { } // IsOp returns whether a user is an operator in this room. -func (r *Room) IsOp(u message.Identifier) bool { +func (r *Room) IsOp(u Member) bool { return r.Ops.In(u.ID()) } diff --git a/chat/room_test.go b/chat/room_test.go index 5f4b02f3..d2ed523a 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -58,7 +58,7 @@ func TestIgnore(t *testing.T) { users := make([]ScreenedUser, 3) for i := 0; i < 3; i++ { screen := &MockScreen{} - user := message.NewUserScreen(message.SimpleID(fmt.Sprintf("user%d", i)), screen) + user := message.NewUserScreen(fmt.Sprintf("user%d", i), screen) users[i] = ScreenedUser{ user: user, screen: screen, @@ -176,7 +176,7 @@ func TestRoomJoin(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen(message.SimpleID("foo"), s) + u := message.NewUserScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -212,7 +212,7 @@ func TestRoomJoin(t *testing.T) { } func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { - u := message.NewUser(message.SimpleID("foo")) + u := message.NewUser("foo") u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -251,7 +251,7 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { } func TestRoomQuietToggleBroadcasts(t *testing.T) { - u := message.NewUser(message.SimpleID("foo")) + u := message.NewUser("foo") u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -294,7 +294,7 @@ func TestQuietToggleDisplayState(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen(message.SimpleID("foo"), s) + u := message.NewUserScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -335,7 +335,7 @@ func TestRoomNames(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen(message.SimpleID("foo"), s) + u := message.NewUserScreen("foo", s) ch := NewRoom() go ch.Serve() diff --git a/chat/set_test.go b/chat/set_test.go index 54955f60..61722a80 100644 --- a/chat/set_test.go +++ b/chat/set_test.go @@ -10,7 +10,7 @@ import ( func TestSet(t *testing.T) { var err error s := set.New() - u := message.NewUser(message.SimpleID("foo")) + u := message.NewUser("foo") if s.In(u.ID()) { t.Errorf("Set should be empty.") @@ -25,7 +25,7 @@ func TestSet(t *testing.T) { t.Errorf("Set should contain user.") } - u2 := message.NewUser(message.SimpleID("bar")) + u2 := message.NewUser("bar") err = s.Add(set.Itemize(u2.ID(), u2)) if err != nil { t.Error(err) diff --git a/client.go b/client.go new file mode 100644 index 00000000..5776cac3 --- /dev/null +++ b/client.go @@ -0,0 +1,45 @@ +package sshchat + +import ( + "net" + "time" + + humanize "github.com/dustin/go-humanize" + "github.com/shazow/ssh-chat/chat/message" + "github.com/shazow/ssh-chat/sshd" +) + +type Client struct { + sshd.Connection + message.User + + connected time.Time +} + +// Whois returns a whois description for non-admin users. +func (client Client) Whois() string { + conn, u := client.Connection, client.User + fingerprint := "(no public key)" + 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()) +} + +// WhoisAdmin returns a whois description for admin users. +func (client Client) WhoisAdmin() string { + conn, u := client.Connection, client.User + 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()) +} diff --git a/host.go b/host.go index ca3425d7..55f7bdce 100644 --- a/host.go +++ b/host.go @@ -80,8 +80,8 @@ func (h *Host) isOp(conn sshd.Connection) bool { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { - ident := toIdentity(term.Conn) - user := message.NewUserScreen(ident, term) + requestedName := term.Conn.Name() + user := message.NewUserScreen(requestedName, term) cfg := user.Config() cfg.Theme = &h.theme user.SetConfig(cfg) @@ -105,7 +105,7 @@ func (h *Host) Connect(term *sshd.Terminal) { member, err := h.Join(user) if err != nil { // Try again... - ident.SetName(fmt.Sprintf("Guest%d", count)) + user.SetName(fmt.Sprintf("Guest%d", count)) member, err = h.Join(user) } if err != nil { @@ -331,34 +331,37 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - c.Add(chat.Command{ - Prefix: "/whois", - PrefixHelp: "USER", - Help: "Information about USER.", - Handler: func(room *chat.Room, msg message.CommandMsg) error { - args := msg.Args() - if len(args) == 0 { - return errors.New("must specify user") - } + // XXX: Temporarily disable whois + /* + c.Add(chat.Command{ + Prefix: "/whois", + PrefixHelp: "USER", + Help: "Information about USER.", + Handler: func(room *chat.Room, msg message.CommandMsg) error { + 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") - } + target, ok := h.GetUser(args[0]) + if !ok { + return errors.New("user not found") + } - id := target.Identifier.(*identity) - var whois string - switch room.IsOp(msg.From()) { - case true: - whois = id.WhoisAdmin() - case false: - whois = id.Whois() - } - room.Send(message.NewSystemMsg(whois, msg.From())) + id := target.Identifier.(*identity) + var whois string + switch room.IsOp(msg.From()) { + case true: + whois = id.WhoisAdmin() + case false: + whois = id.Whois() + } + room.Send(message.NewSystemMsg(whois, msg.From())) - return nil - }, - }) + return nil + }, + }) + */ // Hidden commands c.Add(chat.Command{ @@ -378,41 +381,91 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - // Op commands - 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") - } + // XXX: Temporarily disable op and ban + /* + + 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") - } + 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") - } + var until time.Duration = 0 + if len(args) > 1 { + until, _ = time.ParseDuration(args[1]) + } - body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name()) - room.Send(message.NewAnnounceMsg(body)) - target.Close() - return nil - }, - }) + user, ok := h.GetUser(args[0]) + if !ok { + return errors.New("user not found") + } + room.Ops.Add(set.Keyize(user.ID())) + + h.auth.Op(user.Identifier.(*identity).PublicKey(), until) + body := fmt.Sprintf("Made op by %s.", msg.From().Name()) + room.Send(message.NewSystemMsg(body, user)) + + return nil + }, + }) + + // Op commands + c.Add(chat.Command{ + Op: true, + Prefix: "/ban", + PrefixHelp: "USER [DURATION]", + Help: "Ban USER from the server.", + Handler: func(room *chat.Room, msg message.CommandMsg) error { + // TODO: Would be nice to specify what to ban. Key? Ip? etc. + 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") + } + + var until time.Duration = 0 + if len(args) > 1 { + until, _ = time.ParseDuration(args[1]) + } + + id := target.Identifier.(*identity) + h.auth.Ban(id.PublicKey(), until) + h.auth.BanAddr(id.RemoteAddr(), until) + + body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) + room.Send(message.NewAnnounceMsg(body)) + target.Close() + + logger.Debugf("Banned: \n-> %s", id.Whois()) + + return nil + }, + }) + */ c.Add(chat.Command{ Op: true, - Prefix: "/ban", - PrefixHelp: "USER [DURATION]", - Help: "Ban USER from the server.", + Prefix: "/kick", + PrefixHelp: "USER", + Help: "Kick USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { - // TODO: Would be nice to specify what to ban. Key? Ip? etc. if !room.IsOp(msg.From()) { return errors.New("must be op") } @@ -427,21 +480,9 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("user not found") } - var until time.Duration = 0 - if len(args) > 1 { - until, _ = time.ParseDuration(args[1]) - } - - id := target.Identifier.(*identity) - h.auth.Ban(id.PublicKey(), 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 kicked by %s.", target.Name(), msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) target.Close() - - logger.Debugf("Banned: \n-> %s", id.Whois()) - return nil }, }) @@ -476,38 +517,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]) - } - - user, ok := h.GetUser(args[0]) - if !ok { - return errors.New("user not found") - } - room.Ops.Add(set.Keyize(user.ID())) - - h.auth.Op(user.Identifier.(*identity).PublicKey(), until) - - body := fmt.Sprintf("Made op by %s.", msg.From().Name()) - room.Send(message.NewSystemMsg(body, user)) - - return nil - }, - }) } diff --git a/host_test.go b/host_test.go index 405792e2..784c5215 100644 --- a/host_test.go +++ b/host_test.go @@ -27,7 +27,7 @@ func stripPrompt(s string) string { func TestHostGetPrompt(t *testing.T) { var expected, actual string - u := message.NewUser(&identity{id: "foo"}) + u := message.NewUser("foo") actual = u.Prompt() expected = "[foo] " diff --git a/identity.go b/identity.go deleted file mode 100644 index b33cb42f..00000000 --- a/identity.go +++ /dev/null @@ -1,65 +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 -} - -// Converts an sshd.Connection to an identity. -func toIdentity(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) SetName(name string) { - i.id = chat.SanitizeName(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) -} diff --git a/sanitize.go b/sanitize.go new file mode 100644 index 00000000..130caa33 --- /dev/null +++ b/sanitize.go @@ -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, "") +} From 810ef13bea597c36de5b1d35f1707f915b116a2c Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 7 Sep 2016 15:33:24 -0400 Subject: [PATCH 07/21] set: AddNew vs Add, added Rename --- set/item.go | 14 ++++++++++++++ set/set.go | 49 ++++++++++++++++++++++++++++++++----------------- set/set_test.go | 8 +++++++- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/set/item.go b/set/item.go index d912cf87..d50f6c21 100644 --- a/set/item.go +++ b/set/item.go @@ -29,6 +29,20 @@ 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 func (item StringItem) Key() string { diff --git a/set/set.go b/set/set.go index 1b489ad5..42ed5156 100644 --- a/set/set.go +++ b/set/set.go @@ -55,6 +55,7 @@ func (s *Set) In(key string) bool { s.RUnlock() if ok && item.Value() == nil { s.cleanup(key) + ok = false } return ok } @@ -66,12 +67,13 @@ func (s *Set) Get(key string) (Item, error) { item, ok := s.lookup[key] s.RUnlock() + if ok && item.Value() == nil { + s.cleanup(key) + ok = false + } if !ok { return nil, ErrMissing } - if item.Value() == nil { - s.cleanup(key) - } return item, nil } @@ -87,10 +89,7 @@ func (s *Set) cleanup(key string) { } // Add item to this set if it does not exist already. -func (s *Set) Add(item Item) error { - if item.Value() == nil { - return ErrNil - } +func (s *Set) AddNew(item Item) error { key := s.normalize(item.Key()) s.Lock() @@ -101,7 +100,26 @@ func (s *Set) Add(item Item) error { return ErrCollision } - s.lookup[key] = item + if item.Value() == nil { + delete(s.lookup, key) + } else { + 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 } @@ -123,9 +141,6 @@ func (s *Set) Remove(key string) error { // Replace oldKey with a new item, which might be a new key. // Can be used to rename items. func (s *Set) Replace(oldKey string, item Item) error { - if item.Value() == nil { - return ErrNil - } newKey := s.normalize(item.Key()) oldKey = s.normalize(oldKey) @@ -140,15 +155,15 @@ func (s *Set) Replace(oldKey string, item Item) error { } // Remove oldKey - _, found = s.lookup[oldKey] - if !found { - return ErrMissing - } delete(s.lookup, oldKey) } - // Add new item - s.lookup[newKey] = item + if item.Value() == nil { + delete(s.lookup, newKey) + } else { + // Add new item + s.lookup[newKey] = item + } return nil } diff --git a/set/set_test.go b/set/set_test.go index f75192d8..58c06138 100644 --- a/set/set_test.go +++ b/set/set_test.go @@ -21,10 +21,16 @@ func TestSetExpiring(t *testing.T) { 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() { 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)} if item.Expired() { From 5f2a230ecc5638bbba56dbf4938d84d60660c50d Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Wed, 7 Sep 2016 15:33:44 -0400 Subject: [PATCH 08/21] progress: broken: Multi-client support, tests fail, Whois factored --- client.go | 36 +------- host.go | 269 +++++++++++++++++++++++++++++++----------------------- whois.go | 41 +++++++++ 3 files changed, 200 insertions(+), 146 deletions(-) create mode 100644 whois.go diff --git a/client.go b/client.go index 5776cac3..9f3b9a72 100644 --- a/client.go +++ b/client.go @@ -1,45 +1,15 @@ package sshchat import ( - "net" "time" - humanize "github.com/dustin/go-humanize" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/sshd" ) type Client struct { - sshd.Connection - message.User + user *message.User + conn sshd.Connection - connected time.Time -} - -// Whois returns a whois description for non-admin users. -func (client Client) Whois() string { - conn, u := client.Connection, client.User - fingerprint := "(no public key)" - 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()) -} - -// WhoisAdmin returns a whois description for admin users. -func (client Client) WhoisAdmin() string { - conn, u := client.Connection, client.User - 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()) + timestamp time.Time } diff --git a/host.go b/host.go index 55f7bdce..117af551 100644 --- a/host.go +++ b/host.go @@ -32,9 +32,10 @@ type Host struct { // Default theme theme message.Theme - mu sync.Mutex - motd string - count int + mu sync.Mutex + motd string + count int + clients map[*message.User][]Client } // NewHost creates a Host on top of an existing listener. @@ -45,6 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host { listener: listener, commands: chat.Commands{}, auth: auth, + clients: map[*message.User][]Client{}, } // Make our own commands registry instance. @@ -70,32 +72,29 @@ func (h *Host) SetMotd(motd string) { 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. func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() user := message.NewUserScreen(requestedName, term) + + client := h.addClient(user, term.Conn) + defer h.removeClient(user, client) + + h.mu.Lock() + motd := h.motd + count := h.count + h.count++ + h.mu.Unlock() + cfg := user.Config() cfg.Theme = &h.theme user.SetConfig(cfg) - go user.Consume() // Close term once user is closed. defer user.Close() defer term.Close() - h.mu.Lock() - motd := h.motd - count := h.count - h.count++ - h.mu.Unlock() + go user.Consume() // Send MOTD if motd != "" { @@ -119,11 +118,17 @@ func (h *Host) Connect(term *sshd.Terminal) { user.SetHighlight(user.Name()) // Should the user be op'd on join? - if h.isOp(term.Conn) { - h.Room.Ops.Add(set.Keyize(member.ID())) + if key := term.Conn.PublicKey(); key != nil { + 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()) for { @@ -175,6 +180,41 @@ func (h *Host) Connect(term *sshd.Terminal) { logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) } +func (h *Host) addClient(user *message.User, conn sshd.Connection) *Client { + client := Client{ + user: user, + conn: conn, + timestamp: time.Now(), + } + h.mu.Lock() + h.clients[user] = append(h.clients[user], client) + h.mu.Unlock() + return &client +} + +func (h *Host) removeClient(user *message.User, client *Client) { + h.mu.Lock() + defer h.mu.Unlock() + + clients := h.clients[user] + for i, c := range clients { + // Find the user + if &c != client { + continue + } + // Delete corresponding client + clients[i] = clients[len(clients)-1] + clients = clients[:len(clients)-1] + break + } +} + +func (h *Host) findClients(user *message.User) []Client { + h.mu.Lock() + defer h.mu.Unlock() + return h.clients[user] +} + // Serve our chat room onto the listener func (h *Host) Serve() { h.listener.HandlerFunc = h.Connect @@ -331,37 +371,35 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - // XXX: Temporarily disable whois - /* - c.Add(chat.Command{ - Prefix: "/whois", - PrefixHelp: "USER", - Help: "Information about USER.", - Handler: func(room *chat.Room, msg message.CommandMsg) error { - args := msg.Args() - if len(args) == 0 { - return errors.New("must specify user") - } + c.Add(chat.Command{ + Prefix: "/whois", + PrefixHelp: "USER", + Help: "Information about USER.", + Handler: func(room *chat.Room, msg message.CommandMsg) error { + 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") - } + target, ok := h.GetUser(args[0]) + if !ok { + return errors.New("user not found") + } - id := target.Identifier.(*identity) - var whois string - switch room.IsOp(msg.From()) { - case true: - whois = id.WhoisAdmin() - case false: - whois = id.Whois() - } - room.Send(message.NewSystemMsg(whois, msg.From())) + // FIXME: Handle many clients + clients := h.findClients(target) + var whois string + switch room.IsOp(msg.From()) { + case true: + whois = whoisAdmin(clients) + case false: + whois = whoisPublic(clients) + } + room.Send(message.NewSystemMsg(whois, msg.From())) - return nil - }, - }) - */ + return nil + }, + }) // Hidden commands c.Add(chat.Command{ @@ -381,85 +419,90 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - // XXX: Temporarily disable op and ban - /* - - 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") - } + 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") - } + 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]) - } + var until time.Duration = 0 + if len(args) > 1 { + until, _ = time.ParseDuration(args[1]) + } - user, ok := h.GetUser(args[0]) - if !ok { - return errors.New("user not found") - } + user, ok := h.GetUser(args[0]) + if !ok { + 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())) + } - h.auth.Op(user.Identifier.(*identity).PublicKey(), until) + for _, client := range h.findClients(user) { + h.auth.Op(client.conn.PublicKey(), until) + } - body := fmt.Sprintf("Made op by %s.", msg.From().Name()) - room.Send(message.NewSystemMsg(body, user)) + body := fmt.Sprintf("Made op by %s.", msg.From().Name()) + room.Send(message.NewSystemMsg(body, user)) - return nil - }, - }) - - // Op commands - c.Add(chat.Command{ - Op: true, - Prefix: "/ban", - PrefixHelp: "USER [DURATION]", - Help: "Ban USER from the server.", - Handler: func(room *chat.Room, msg message.CommandMsg) error { - // TODO: Would be nice to specify what to ban. Key? Ip? etc. - if !room.IsOp(msg.From()) { - return errors.New("must be op") - } + return nil + }, + }) - args := msg.Args() - if len(args) == 0 { - return errors.New("must specify user") - } + // Op commands + c.Add(chat.Command{ + Op: true, + Prefix: "/ban", + PrefixHelp: "USER [DURATION]", + Help: "Ban USER from the server.", + Handler: func(room *chat.Room, msg message.CommandMsg) error { + // TODO: Would be nice to specify what to ban. Key? Ip? etc. + if !room.IsOp(msg.From()) { + return errors.New("must be op") + } - target, ok := h.GetUser(args[0]) - if !ok { - return errors.New("user not found") - } + 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]) - } + target, ok := h.GetUser(args[0]) + if !ok { + return errors.New("user not found") + } - id := target.Identifier.(*identity) - h.auth.Ban(id.PublicKey(), until) - h.auth.BanAddr(id.RemoteAddr(), until) + var until time.Duration = 0 + if len(args) > 1 { + until, _ = time.ParseDuration(args[1]) + } - body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) - room.Send(message.NewAnnounceMsg(body)) - target.Close() + clients := h.findClients(target) + for _, client := range clients { + h.auth.Ban(client.conn.PublicKey(), until) + h.auth.BanAddr(client.conn.RemoteAddr(), until) + } - logger.Debugf("Banned: \n-> %s", id.Whois()) + body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) + room.Send(message.NewAnnounceMsg(body)) + target.Close() + + logger.Debugf("Banned: \n-> %s", whoisAdmin(clients)) + + return nil + }, + }) - return nil - }, - }) - */ c.Add(chat.Command{ Op: true, Prefix: "/kick", diff --git a/whois.go b/whois.go new file mode 100644 index 00000000..7089667d --- /dev/null +++ b/whois.go @@ -0,0 +1,41 @@ +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(clients []Client) string { + // FIXME: Handle many clients + conn, u := clients[0].conn, clients[0].user + + fingerprint := "(no public key)" + 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(clients []Client) string { + // FIXME: Handle many clients + conn, u := clients[0].conn, clients[0].user + + 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()) +} From 01b989cab2ff515c33a8153cf9e3a5a4c2ec0f65 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 8 Sep 2016 14:09:43 -0400 Subject: [PATCH 09/21] chat/message/user: screen -> WriteCloser embedded --- chat/message/user.go | 18 ++++++++---------- chat/message/user_test.go | 9 ++++++--- chat/set_test.go | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/chat/message/user.go b/chat/message/user.go index d10d02b5..fd656b2d 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -18,13 +18,13 @@ var ErrUserClosed = errors.New("user closed") // User definition, implemented set Item interface and io.Writer type User struct { - colorIdx int - joined time.Time - msg chan Message - done chan struct{} + io.WriteCloser - screen io.WriteCloser + colorIdx int + joined time.Time closeOnce sync.Once + msg chan Message + done chan struct{} mu sync.Mutex name string @@ -47,7 +47,7 @@ func NewUser(name string) *User { func NewUserScreen(name string, screen io.WriteCloser) *User { u := NewUser(name) - u.screen = screen + u.WriteCloser = screen return u } @@ -112,9 +112,7 @@ func (u *User) setColorIdx(idx int) { // Disconnect user, stop accepting messages func (u *User) Close() { u.closeOnce.Do(func() { - if u.screen != nil { - u.screen.Close() - } + u.WriteCloser.Close() // close(u.msg) TODO: Close? close(u.done) }) @@ -182,7 +180,7 @@ func (u *User) render(m Message) string { // 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)) + _, err := u.Write([]byte(r)) if err != nil { logger.Printf("Write failed to %s, closing: %s", u.Name(), err) u.Close() diff --git a/chat/message/user_test.go b/chat/message/user_test.go index 871730d9..437015e6 100644 --- a/chat/message/user_test.go +++ b/chat/message/user_test.go @@ -13,12 +13,15 @@ func TestMakeUser(t *testing.T) { m := NewAnnounceMsg("hello") defer u.Close() - u.Send(m) - u.HandleMsg(u.ConsumeOne()) + err := u.Send(m) + if err != nil { + t.Fatalf("failed to send: %s", err) + } + u.HandleMsg(<-u.msg) s.Read(&actual) expected = []byte(m.String() + Newline) if !reflect.DeepEqual(actual, expected) { - t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) + t.Errorf("Got: %q; Expected: %q", actual, expected) } } diff --git a/chat/set_test.go b/chat/set_test.go index 61722a80..754eb602 100644 --- a/chat/set_test.go +++ b/chat/set_test.go @@ -31,9 +31,9 @@ func TestSet(t *testing.T) { t.Error(err) } - err = s.Add(set.Itemize(u2.ID(), u2)) + err = s.AddNew(set.Itemize(u2.ID(), u2)) if err != set.ErrCollision { - t.Error(err) + t.Errorf("expected ErrCollision, got: %s", err) } size := s.Len() From a22d9380e413a23341103a13385227aef729a8ea Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 9 Sep 2016 12:11:27 -0400 Subject: [PATCH 10/21] refactor: fixed failing tests --- chat/room.go | 2 +- host.go | 3 +++ host_test.go | 24 ++++++++++++++++++------ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/chat/room.go b/chat/room.go index 1bf85db3..e94f4547 100644 --- a/chat/room.go +++ b/chat/room.go @@ -143,7 +143,7 @@ func (r *Room) Join(m Member) (*roomMember, error) { Member: m, Ignored: set.New(), } - err := r.Members.Add(set.Itemize(m.ID(), member)) + err := r.Members.AddNew(set.Itemize(m.ID(), member)) if err != nil { return nil, err } diff --git a/host.go b/host.go index 117af551..dfc7d553 100644 --- a/host.go +++ b/host.go @@ -187,6 +187,9 @@ func (h *Host) addClient(user *message.User, conn sshd.Connection) *Client { timestamp: time.Now(), } h.mu.Lock() + if _, ok := h.clients[user]; ok { + logger.Warningf("user collision: %q", user) + } h.clients[user] = append(h.clients[user], client) h.mu.Unlock() return &client diff --git a/host_test.go b/host_test.go index 784c5215..2e932776 100644 --- a/host_test.go +++ b/host_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "crypto/rsa" "errors" + "fmt" "io" "io/ioutil" "strings" @@ -63,6 +64,8 @@ func TestHostNameCollision(t *testing.T) { done := make(chan struct{}, 1) + canary := "canarystring" + // First client go func() { 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) } + fmt.Fprintf(w, canary+message.Newline) + + <-done // Wrap it up. - close(done) return nil }) if err != nil { @@ -108,16 +113,23 @@ func TestHostNameCollision(t *testing.T) { err = sshd.ConnectShell(s.Addr().String(), "foo", func(r io.Reader, w io.WriteCloser) error { scanner := bufio.NewScanner(r) - // Consume the initial buffer - scanner.Scan() - scanner.Scan() - scanner.Scan() + // Scan until we see our canarystring + for scanner.Scan() { + s := scanner.Text() + 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() if !strings.HasPrefix(actual, "[Guest1] ") { - // FIXME: Flaky? t.Errorf("Second client did not get Guest1 name: %q", actual) } + close(done) return nil }) if err != nil { From 9ecd2a6fa2a88f49ac333c2bfd19c636143285f0 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 9 Sep 2016 12:16:19 -0400 Subject: [PATCH 11/21] set: Fix test flake --- set/set_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/set/set_test.go b/set/set_test.go index 58c06138..13147b2c 100644 --- a/set/set_test.go +++ b/set/set_test.go @@ -1,6 +1,7 @@ package set import ( + "strings" "testing" "time" ) @@ -80,8 +81,13 @@ func TestSetExpiring(t *testing.T) { t.Errorf("failed to get barbar: %s", err) } b := s.ListPrefix("b") - if len(b) != 2 || b[0].Key() != "bar" || b[1].Key() != "barbar" { - t.Errorf("b-prefix incorrect: %q", b) + if len(b) != 2 { + 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 { From 33a76bb7f49efcf640c1b972d3425fac8dfd874c Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sat, 10 Sep 2016 20:36:23 -0400 Subject: [PATCH 12/21] broken: Use Member rather than message.User when possible; chat tests are broken. --- chat/command.go | 18 +++---- chat/member.go | 4 +- chat/message/message.go | 33 +++++++----- chat/message/theme.go | 4 +- chat/message/user.go | 113 ++++++++++++++++++++-------------------- chat/room.go | 7 +-- chat/room_test.go | 16 +----- 7 files changed, 94 insertions(+), 101 deletions(-) diff --git a/chat/command.go b/chat/command.go index 1d8e6fb6..1b518bae 100644 --- a/chat/command.go +++ b/chat/command.go @@ -5,6 +5,7 @@ package chat import ( "errors" "fmt" + "io" "strings" "github.com/shazow/ssh-chat/chat/message" @@ -110,7 +111,7 @@ func InitCommands(c *Commands) { c.Add(Command{ Prefix: "/help", Handler: func(room *Room, msg message.CommandMsg) error { - op := room.IsOp(msg.From()) + op := room.IsOp(msg.From().(Member)) room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From())) return nil }, @@ -135,8 +136,7 @@ func InitCommands(c *Commands) { Prefix: "/exit", Help: "Exit the chat.", Handler: func(room *Room, msg message.CommandMsg) error { - msg.From().Close() - return nil + return msg.From().(io.Closer).Close() }, }) c.Alias("/exit", "/quit") @@ -150,7 +150,7 @@ func InitCommands(c *Commands) { if len(args) != 1 { return ErrMissingArg } - member, ok := room.MemberByID(msg.From().ID()) + member, ok := room.MemberByID(msg.From().(Member).ID()) if !ok { return ErrMissingMember } @@ -184,7 +184,7 @@ func InitCommands(c *Commands) { PrefixHelp: "[colors|...]", Help: "Set your color theme. (More themes: solarized, mono, hacker)", Handler: func(room *Room, msg message.CommandMsg) error { - user := msg.From() + user := msg.From().(Member) args := msg.Args() cfg := user.Config() if len(args) == 0 { @@ -215,7 +215,7 @@ func InitCommands(c *Commands) { Prefix: "/quiet", Help: "Silence room announcements.", Handler: func(room *Room, msg message.CommandMsg) error { - u := msg.From() + u := msg.From().(Member) cfg := u.Config() cfg.Quiet = !cfg.Quiet u.SetConfig(cfg) @@ -253,7 +253,7 @@ func InitCommands(c *Commands) { PrefixHelp: "[USER]", Help: "Hide messages from USER, /unignore USER to stop hiding.", Handler: func(room *Room, msg message.CommandMsg) error { - from, ok := room.Member(msg.From()) + from, ok := room.Member(msg.From().(Member)) if !ok { return ErrMissingMember } @@ -278,7 +278,7 @@ func InitCommands(c *Commands) { return nil } - if id == msg.From().ID() { + if id == msg.From().(Member).ID() { return errors.New("cannot ignore self") } target, ok := room.MemberByID(id) @@ -302,7 +302,7 @@ func InitCommands(c *Commands) { Prefix: "/unignore", PrefixHelp: "USER", Handler: func(room *Room, msg message.CommandMsg) error { - from, ok := room.Member(msg.From()) + from, ok := room.Member(msg.From().(Member)) if !ok { return ErrMissingMember } diff --git a/chat/member.go b/chat/member.go index f0d3b1cb..471cdd41 100644 --- a/chat/member.go +++ b/chat/member.go @@ -12,9 +12,9 @@ type roomMember struct { } type Member interface { - ID() string + message.Author - Name() string + ID() string SetName(string) Config() message.UserConfig diff --git a/chat/message/message.go b/chat/message/message.go index 5b4f76a7..891e6ab2 100644 --- a/chat/message/message.go +++ b/chat/message/message.go @@ -6,6 +6,11 @@ import ( "time" ) +type Author interface { + Name() string + Color() int +} + // Message is an interface for messages. type Message interface { Render(*Theme) string @@ -16,15 +21,15 @@ type Message interface { type MessageTo interface { Message - To() *User + To() Author } type MessageFrom interface { Message - From() *User + From() Author } -func ParseInput(body string, from *User) Message { +func ParseInput(body string, from Author) Message { m := NewPublicMsg(body, from) cmd, isCmd := m.ParseCommand() if isCmd { @@ -69,10 +74,10 @@ func (m Msg) Timestamp() time.Time { // PublicMsg is any message from a user sent to the room. type PublicMsg struct { Msg - from *User + from Author } -func NewPublicMsg(body string, from *User) PublicMsg { +func NewPublicMsg(body string, from Author) PublicMsg { return PublicMsg{ Msg: Msg{ body: body, @@ -82,7 +87,7 @@ func NewPublicMsg(body string, from *User) PublicMsg { } } -func (m PublicMsg) From() *User { +func (m PublicMsg) From() Author { return m.from } @@ -137,10 +142,10 @@ func (m PublicMsg) String() string { // sender to see the emote. type EmoteMsg struct { Msg - from *User + from Author } -func NewEmoteMsg(body string, from *User) *EmoteMsg { +func NewEmoteMsg(body string, from Author) *EmoteMsg { return &EmoteMsg{ Msg: Msg{ body: body, @@ -161,17 +166,17 @@ func (m EmoteMsg) String() string { // PrivateMsg is a message sent to another user, not shown to anyone else. type PrivateMsg struct { 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{ PublicMsg: NewPublicMsg(body, from), to: to, } } -func (m PrivateMsg) To() *User { +func (m PrivateMsg) To() Author { return m.to } @@ -191,10 +196,10 @@ func (m PrivateMsg) String() string { // to anyone else. Usually in response to something, like /help. type SystemMsg struct { Msg - to *User + to Author } -func NewSystemMsg(body string, to *User) *SystemMsg { +func NewSystemMsg(body string, to Author) *SystemMsg { return &SystemMsg{ Msg: Msg{ body: body, @@ -215,7 +220,7 @@ func (m *SystemMsg) String() string { return fmt.Sprintf("-> %s", m.body) } -func (m *SystemMsg) To() *User { +func (m *SystemMsg) To() Author { return m.to } diff --git a/chat/message/theme.go b/chat/message/theme.go index d2585b75..7ff8a727 100644 --- a/chat/message/theme.go +++ b/chat/message/theme.go @@ -127,12 +127,12 @@ func (t Theme) ID() string { } // Colorize name string given some index -func (t Theme) ColorName(u *User) string { +func (t Theme) ColorName(u Author) string { if t.names == nil { 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 diff --git a/chat/message/user.go b/chat/message/user.go index fd656b2d..7c9c2acc 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -16,10 +16,53 @@ const reHighlight = `\b(%s)\b` var ErrUserClosed = errors.New("user closed") -// User definition, implemented set Item interface and io.Writer -type User struct { +// User container that knows about writing to an IO screen. +type UserScreen struct { + *User io.WriteCloser +} + +func (u *UserScreen) Close() error { + u.User.Close() + return u.WriteCloser.Close() +} + +// HandleMsg will render the message to the screen, blocking. +func (u *UserScreen) HandleMsg(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.User.Close() + u.WriteCloser.Close() + } + return err +} +// Consume message buffer into the handler. Will block, should be called in a +// goroutine. +func (u *UserScreen) 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 +// TODO: Stop using it and remove it. +func (u *UserScreen) ConsumeOne() Message { + return <-u.msg +} + +// User definition, implemented set Item interface and io.Writer +type User struct { colorIdx int joined time.Time closeOnce sync.Once @@ -29,7 +72,7 @@ type User struct { mu sync.Mutex name string config UserConfig - replyTo *User // Set when user gets a /msg, for replying. + replyTo Author // Set when user gets a /msg, for replying. } func NewUser(name string) *User { @@ -45,11 +88,11 @@ func NewUser(name string) *User { return &u } -func NewUserScreen(name string, screen io.WriteCloser) *User { - u := NewUser(name) - u.WriteCloser = screen - - return u +func NewUserScreen(name string, screen io.WriteCloser) *UserScreen { + return &UserScreen{ + User: NewUser(name), + WriteCloser: screen, + } } func (u *User) Config() UserConfig { @@ -70,6 +113,10 @@ func (u *User) ID() string { return SanitizeName(u.name) } +func (u *User) Color() int { + return u.colorIdx +} + func (u *User) Name() string { u.mu.Lock() defer u.mu.Unlock() @@ -89,14 +136,14 @@ func (u *User) SetName(name string) { } // ReplyTo returns the last user that messaged this user. -func (u *User) ReplyTo() *User { +func (u *User) ReplyTo() Author { 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) { +func (u *User) SetReplyTo(user Author) { // TODO: Use UserConfig.ReplyTo string u.mu.Lock() defer u.mu.Unlock() @@ -112,46 +159,11 @@ func (u *User) setColorIdx(idx int) { // Disconnect user, stop accepting messages func (u *User) Close() { u.closeOnce.Do(func() { - u.WriteCloser.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 -// TODO: Stop using it and remove it. -func (u *User) ConsumeOne() Message { - return <-u.msg -} - -// Check if there are pending messages, used for testing -// TODO: Stop using it and remove it. -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)) @@ -177,17 +189,6 @@ func (u *User) render(m Message) string { } } -// HandleMsg will render the message to the screen, blocking. -func (u *User) HandleMsg(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 -} - // Add message to consume by user func (u *User) Send(m Message) error { select { diff --git a/chat/room.go b/chat/room.go index e94f4547..0e4e33f8 100644 --- a/chat/room.go +++ b/chat/room.go @@ -78,21 +78,22 @@ func (r *Room) HandleMsg(m message.Message) { go r.HandleMsg(m) } case message.MessageTo: - user := m.To() + user := m.To().(Member) user.Send(m) default: fromMsg, skip := m.(message.MessageFrom) var skipUser Member if skip { - skipUser = fromMsg.From() + skipUser = fromMsg.From().(Member) } r.history.Add(m) r.Members.Each(func(_ string, item set.Item) (err error) { roomMember := item.Value().(*roomMember) user := roomMember.Member + from := fromMsg.From().(Member) - if fromMsg != nil && roomMember.Ignored.In(fromMsg.From().ID()) { + if fromMsg != nil && roomMember.Ignored.In(from.ID()) { // Skip because ignored return } diff --git a/chat/room_test.go b/chat/room_test.go index d2ed523a..eb215c73 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -1,7 +1,6 @@ package chat import ( - "errors" "reflect" "testing" @@ -42,7 +41,7 @@ func TestRoomServe(t *testing.T) { } type ScreenedUser struct { - user *message.User + *message.User screen *MockScreen } @@ -159,19 +158,6 @@ 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) { var expected, actual []byte From 50022e9e449749798d8dbc33b387120ed3109275 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 11 Sep 2016 16:48:50 -0400 Subject: [PATCH 13/21] broken: Split UserScreen into PipedScreen, BufferedScreen, HandledScreen --- chat/message/config.go | 24 +++++ chat/message/screen.go | 206 +++++++++++++++++++++++++++++++++++++ chat/message/theme_test.go | 2 +- chat/message/user.go | 206 ++----------------------------------- chat/message/user_test.go | 4 +- chat/room.go | 2 +- chat/room_test.go | 49 ++++----- host.go | 2 +- 8 files changed, 263 insertions(+), 232 deletions(-) create mode 100644 chat/message/config.go create mode 100644 chat/message/screen.go diff --git a/chat/message/config.go b/chat/message/config.go new file mode 100644 index 00000000..9cb8af2d --- /dev/null +++ b/chat/message/config.go @@ -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? +} diff --git a/chat/message/screen.go b/chat/message/screen.go new file mode 100644 index 00000000..c26bd3bb --- /dev/null +++ b/chat/message/screen.go @@ -0,0 +1,206 @@ +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: &baseScreen{ + User: NewUser(name), + }, + WriteCloser: screen, + } +} + +func HandledScreen(name string, handler func(Message) error) *handledScreen { + return &handledScreen{ + baseScreen: &baseScreen{ + User: NewUser(name), + }, + handler: handler, + } +} + +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) + } + } +} diff --git a/chat/message/theme_test.go b/chat/message/theme_test.go index d3d45774..c16222f6 100644 --- a/chat/message/theme_test.go +++ b/chat/message/theme_test.go @@ -52,7 +52,7 @@ func TestTheme(t *testing.T) { } u := NewUser("foo") - u.colorIdx = 4 + u.config.Seed = 4 actual = colorTheme.ColorName(u) expected = "\033[38;05;5mfoo\033[0m" if actual != expected { diff --git a/chat/message/user.go b/chat/message/user.go index 7c9c2acc..022f577d 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -1,75 +1,16 @@ package message import ( - "errors" - "fmt" - "io" "math/rand" - "regexp" - "sync" "time" ) -const messageBuffer = 5 -const messageTimeout = 5 * time.Second const reHighlight = `\b(%s)\b` -var ErrUserClosed = errors.New("user closed") - -// User container that knows about writing to an IO screen. -type UserScreen struct { - *User - io.WriteCloser -} - -func (u *UserScreen) Close() error { - u.User.Close() - return u.WriteCloser.Close() -} - -// HandleMsg will render the message to the screen, blocking. -func (u *UserScreen) HandleMsg(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.User.Close() - u.WriteCloser.Close() - } - return err -} - -// Consume message buffer into the handler. Will block, should be called in a -// goroutine. -func (u *UserScreen) 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 -// TODO: Stop using it and remove it. -func (u *UserScreen) ConsumeOne() Message { - return <-u.msg -} - // User definition, implemented set Item interface and io.Writer type User struct { - colorIdx int - joined time.Time - closeOnce sync.Once - msg chan Message - done chan struct{} + joined time.Time - mu sync.Mutex name string config UserConfig replyTo Author // Set when user gets a /msg, for replying. @@ -80,155 +21,20 @@ func NewUser(name string) *User { name: name, config: DefaultUserConfig, joined: time.Now(), - msg: make(chan Message, messageBuffer), - done: make(chan struct{}), } - u.setColorIdx(rand.Int()) + u.config.Seed = rand.Int() return &u } -func NewUserScreen(name string, screen io.WriteCloser) *UserScreen { - return &UserScreen{ - User: NewUser(name), - WriteCloser: screen, - } -} - -func (u *User) Config() UserConfig { - u.mu.Lock() - defer u.mu.Unlock() - return u.config -} - -func (u *User) SetConfig(cfg UserConfig) { - u.mu.Lock() - u.config = cfg - u.mu.Unlock() -} - -func (u *User) ID() string { - u.mu.Lock() - defer u.mu.Unlock() - return SanitizeName(u.name) -} - -func (u *User) Color() int { - return u.colorIdx -} - func (u *User) Name() string { - u.mu.Lock() - defer u.mu.Unlock() return u.name } -func (u *User) Joined() time.Time { - return u.joined -} - -// Rename the user with a new Identifier. -func (u *User) SetName(name string) { - u.mu.Lock() - u.name = name - u.setColorIdx(rand.Int()) - u.mu.Unlock() -} - -// ReplyTo returns the last user that messaged this user. -func (u *User) ReplyTo() Author { - u.mu.Lock() - defer u.mu.Unlock() - return u.replyTo -} - -// SetReplyTo sets the last user to message this user. -func (u *User) SetReplyTo(user Author) { - // TODO: Use UserConfig.ReplyTo string - 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() { - // close(u.msg) TODO: Close? - close(u.done) - }) -} - -// 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 - } -} - -// 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 -} - -// Prompt renders a theme-colorized prompt string. -func (u *User) Prompt() string { - name := u.Name() - cfg := u.Config() - if cfg.Theme != nil { - name = cfg.Theme.ColorName(u) - } - return fmt.Sprintf("[%s] ", name) -} - -// Container for per-user configurations. -type UserConfig struct { - Highlight *regexp.Regexp - Bell bool - Quiet bool - Theme *Theme +func (u *User) Color() int { + return u.config.Seed } -// Default user configuration to use -var DefaultUserConfig UserConfig - -func init() { - DefaultUserConfig = UserConfig{ - Bell: true, - Quiet: false, - } - - // TODO: Seed random? +func (u *User) ID() string { + return SanitizeName(u.name) } diff --git a/chat/message/user_test.go b/chat/message/user_test.go index 437015e6..6e7d8b02 100644 --- a/chat/message/user_test.go +++ b/chat/message/user_test.go @@ -9,7 +9,8 @@ func TestMakeUser(t *testing.T) { var actual, expected []byte s := &MockScreen{} - u := NewUserScreen("foo", s) + u := PipedScreen("foo", s) + m := NewAnnounceMsg("hello") defer u.Close() @@ -17,7 +18,6 @@ func TestMakeUser(t *testing.T) { if err != nil { t.Fatalf("failed to send: %s", err) } - u.HandleMsg(<-u.msg) s.Read(&actual) expected = []byte(m.String() + Newline) diff --git a/chat/room.go b/chat/room.go index 0e4e33f8..9616807c 100644 --- a/chat/room.go +++ b/chat/room.go @@ -88,7 +88,7 @@ func (r *Room) HandleMsg(m message.Message) { } 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) { roomMember := item.Value().(*roomMember) user := roomMember.Member from := fromMsg.From().(Member) diff --git a/chat/room_test.go b/chat/room_test.go index eb215c73..e1b9d08b 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -57,7 +57,7 @@ func TestIgnore(t *testing.T) { users := make([]ScreenedUser, 3) for i := 0; i < 3; i++ { screen := &MockScreen{} - user := message.NewUserScreen(fmt.Sprintf("user%d", i), screen) + user := message.NewScreen(fmt.Sprintf("user%d", i), screen) users[i] = ScreenedUser{ user: user, screen: screen, @@ -162,7 +162,7 @@ func TestRoomJoin(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen("foo", s) + u := message.PipedScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -173,7 +173,6 @@ func TestRoomJoin(t *testing.T) { t.Fatal(err) } - u.HandleMsg(u.ConsumeOne()) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -181,7 +180,6 @@ func TestRoomJoin(t *testing.T) { } ch.Send(message.NewSystemMsg("hello", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> hello" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -189,7 +187,6 @@ func TestRoomJoin(t *testing.T) { } ch.Send(message.ParseInput("/me says hello.", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("** foo says hello." + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -198,7 +195,11 @@ func TestRoomJoin(t *testing.T) { } func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { - u := message.NewUser("foo") + msgs := make(chan message.Message) + u := message.HandledScreen("foo", func(m message.Message) error { + msgs <- m + return nil + }) u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -211,19 +212,12 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { t.Fatal(err) } - // Drain the initial Join message - <-ch.broadcast - go func() { - /* - for { - msg := u.ConsumeChan() - if _, ok := msg.(*message.AnnounceMsg); ok { - t.Errorf("Got unexpected `%T`", msg) - } + for msg := range msgs { + if _, ok := msg.(*message.AnnounceMsg); ok { + t.Errorf("Got unexpected `%T`", msg) } - */ - // XXX: Fix this + } }() // Call with an AnnounceMsg and all the other types @@ -234,10 +228,16 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { ch.HandleMsg(message.NewSystemMsg("hello", u)) ch.HandleMsg(message.NewPrivateMsg("hello", u, 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) { - u := message.NewUser("foo") + msgs := make(chan message.Message) + u := message.HandledScreen("foo", func(m message.Message) error { + msgs <- m + return nil + }) u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -259,7 +259,7 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { expectedMsg := message.NewAnnounceMsg("Ignored") ch.HandleMsg(expectedMsg) - msg := u.ConsumeOne() + msg := <-msgs if _, ok := msg.(*message.AnnounceMsg); !ok { t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg) } @@ -270,7 +270,7 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { ch.HandleMsg(message.NewAnnounceMsg("Ignored")) ch.HandleMsg(message.NewSystemMsg("hello", u)) - msg = u.ConsumeOne() + msg = <-msgs if _, ok := msg.(*message.AnnounceMsg); ok { t.Errorf("Got unexpected `%T`", msg) } @@ -280,7 +280,7 @@ func TestQuietToggleDisplayState(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen("foo", s) + u := message.PipedScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -291,7 +291,6 @@ func TestQuietToggleDisplayState(t *testing.T) { t.Fatal(err) } - u.HandleMsg(u.ConsumeOne()) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -300,7 +299,6 @@ func TestQuietToggleDisplayState(t *testing.T) { ch.Send(message.ParseInput("/quiet", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> Quiet mode is toggled ON" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -309,7 +307,6 @@ func TestQuietToggleDisplayState(t *testing.T) { ch.Send(message.ParseInput("/quiet", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> Quiet mode is toggled OFF" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -321,7 +318,7 @@ func TestRoomNames(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen("foo", s) + u := message.PipedScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -332,7 +329,6 @@ func TestRoomNames(t *testing.T) { t.Fatal(err) } - u.HandleMsg(u.ConsumeOne()) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -341,7 +337,6 @@ func TestRoomNames(t *testing.T) { ch.Send(message.ParseInput("/names", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> 1 connected: foo" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { diff --git a/host.go b/host.go index dfc7d553..89d72ce7 100644 --- a/host.go +++ b/host.go @@ -75,7 +75,7 @@ func (h *Host) SetMotd(motd string) { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() - user := message.NewUserScreen(requestedName, term) + user := message.NewScreen(requestedName, term) client := h.addClient(user, term.Conn) defer h.removeClient(user, client) From 0aaaa6a1fb90c1421e39440956869699d61276cd Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 13 Sep 2016 18:38:30 -0400 Subject: [PATCH 14/21] progress: chat tests pass --- chat/room.go | 3 +-- chat/room_test.go | 62 +++++++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/chat/room.go b/chat/room.go index 9616807c..790770c3 100644 --- a/chat/room.go +++ b/chat/room.go @@ -91,9 +91,8 @@ func (r *Room) HandleMsg(m message.Message) { r.Members.Each(func(k string, item set.Item) (err error) { roomMember := item.Value().(*roomMember) user := roomMember.Member - from := fromMsg.From().(Member) - if fromMsg != nil && roomMember.Ignored.In(from.ID()) { + if fromMsg != nil && fromMsg.From() != nil && roomMember.Ignored.In(fromMsg.From().(Member).ID()) { // Skip because ignored return } diff --git a/chat/room_test.go b/chat/room_test.go index e1b9d08b..7d6ced5d 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -7,23 +7,17 @@ import ( "github.com/shazow/ssh-chat/chat/message" ) -// Used for testing -type MockScreen struct { - buffer []byte +type ChannelWriter struct { + Chan chan []byte } -func (s *MockScreen) Write(data []byte) (n int, err error) { - s.buffer = append(s.buffer, data...) +func (w *ChannelWriter) Write(data []byte) (n int, err error) { + w.Chan <- 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 (s *MockScreen) Close() error { +func (w *ChannelWriter) Close() error { + close(w.Chan) return nil } @@ -40,11 +34,6 @@ func TestRoomServe(t *testing.T) { } } -type ScreenedUser struct { - *message.User - screen *MockScreen -} - /* func TestIgnore(t *testing.T) { var buffer []byte @@ -161,7 +150,9 @@ func expectOutput(t *testing.T, buffer []byte, expected string) { func TestRoomJoin(t *testing.T) { var expected, actual []byte - s := &MockScreen{} + s := &ChannelWriter{ + Chan: make(chan []byte), + } u := message.PipedScreen("foo", s) ch := NewRoom() @@ -174,21 +165,21 @@ func TestRoomJoin(t *testing.T) { } expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } ch.Send(message.NewSystemMsg("hello", u)) expected = []byte("-> hello" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } ch.Send(message.ParseInput("/me says hello.", u)) 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) } @@ -250,15 +241,12 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { t.Fatal(err) } - // Drain the initial Join message - <-ch.broadcast - u.SetConfig(message.UserConfig{ Quiet: false, }) expectedMsg := message.NewAnnounceMsg("Ignored") - ch.HandleMsg(expectedMsg) + go ch.HandleMsg(expectedMsg) msg := <-msgs if _, ok := msg.(*message.AnnounceMsg); !ok { t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg) @@ -268,8 +256,10 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { Quiet: true, }) - ch.HandleMsg(message.NewAnnounceMsg("Ignored")) - ch.HandleMsg(message.NewSystemMsg("hello", u)) + go func() { + ch.HandleMsg(message.NewAnnounceMsg("Ignored")) + ch.HandleMsg(message.NewSystemMsg("hello", u)) + }() msg = <-msgs if _, ok := msg.(*message.AnnounceMsg); ok { t.Errorf("Got unexpected `%T`", msg) @@ -279,7 +269,9 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { func TestQuietToggleDisplayState(t *testing.T) { var expected, actual []byte - s := &MockScreen{} + s := &ChannelWriter{ + Chan: make(chan []byte), + } u := message.PipedScreen("foo", s) ch := NewRoom() @@ -292,7 +284,7 @@ func TestQuietToggleDisplayState(t *testing.T) { } expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } @@ -300,7 +292,7 @@ func TestQuietToggleDisplayState(t *testing.T) { ch.Send(message.ParseInput("/quiet", u)) expected = []byte("-> Quiet mode is toggled ON" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } @@ -308,7 +300,7 @@ func TestQuietToggleDisplayState(t *testing.T) { ch.Send(message.ParseInput("/quiet", u)) expected = []byte("-> Quiet mode is toggled OFF" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } @@ -317,7 +309,9 @@ func TestQuietToggleDisplayState(t *testing.T) { func TestRoomNames(t *testing.T) { var expected, actual []byte - s := &MockScreen{} + s := &ChannelWriter{ + Chan: make(chan []byte), + } u := message.PipedScreen("foo", s) ch := NewRoom() @@ -330,7 +324,7 @@ func TestRoomNames(t *testing.T) { } expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } @@ -338,7 +332,7 @@ func TestRoomNames(t *testing.T) { ch.Send(message.ParseInput("/names", u)) expected = []byte("-> 1 connected: foo" + message.Newline) - s.Read(&actual) + actual = <-s.Chan if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: %q; Expected: %q", actual, expected) } From 0096404142bbd1dd486352f446319c49e2bc0dcd Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 13 Sep 2016 19:08:37 -0400 Subject: [PATCH 15/21] tests: Add chat.TestIgnore again --- chat/room_test.go | 194 +++++++++++++++++++++------------------------- 1 file changed, 88 insertions(+), 106 deletions(-) diff --git a/chat/room_test.go b/chat/room_test.go index 7d6ced5d..2a0c8770 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -34,112 +34,6 @@ func TestRoomServe(t *testing.T) { } } -/* -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.NewScreen(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) { bytes := []byte(expected) if !reflect.DeepEqual(buffer, bytes) { @@ -185,6 +79,94 @@ func TestRoomJoin(t *testing.T) { } } +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) { + t.Errorf("Got: %q; Expected: %q", actual, expected) + } +} + func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { msgs := make(chan message.Message) u := message.HandledScreen("foo", func(m message.Message) error { From 3ef404198d43291dccf1bc835f04068659e24694 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 13 Sep 2016 21:51:27 -0400 Subject: [PATCH 16/21] tests: All passing again --- chat/message/screen.go | 16 +++++++++------- client.go | 8 +++++++- host.go | 37 ++++++++++++++++++------------------- host_test.go | 4 ++-- whois.go | 9 +++++++-- 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/chat/message/screen.go b/chat/message/screen.go index c26bd3bb..4027f03e 100644 --- a/chat/message/screen.go +++ b/chat/message/screen.go @@ -25,19 +25,21 @@ func BufferedScreen(name string, screen io.WriteCloser) *bufferedScreen { func PipedScreen(name string, screen io.WriteCloser) *pipedScreen { return &pipedScreen{ - baseScreen: &baseScreen{ - User: NewUser(name), - }, + baseScreen: Screen(name), WriteCloser: screen, } } func HandledScreen(name string, handler func(Message) error) *handledScreen { return &handledScreen{ - baseScreen: &baseScreen{ - User: NewUser(name), - }, - handler: handler, + baseScreen: Screen(name), + handler: handler, + } +} + +func Screen(name string) *baseScreen { + return &baseScreen{ + User: NewUser(name), } } diff --git a/client.go b/client.go index 9f3b9a72..d602ead5 100644 --- a/client.go +++ b/client.go @@ -3,13 +3,19 @@ package sshchat import ( "time" + "github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/sshd" ) type Client struct { - user *message.User + user chat.Member conn sshd.Connection timestamp time.Time } + +type Replier interface { + ReplyTo() message.Author + SetReplyTo(message.Author) +} diff --git a/host.go b/host.go index 89d72ce7..9feb022a 100644 --- a/host.go +++ b/host.go @@ -35,7 +35,7 @@ type Host struct { mu sync.Mutex motd string count int - clients map[*message.User][]Client + clients map[chat.Member][]Client } // NewHost creates a Host on top of an existing listener. @@ -46,7 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host { listener: listener, commands: chat.Commands{}, auth: auth, - clients: map[*message.User][]Client{}, + clients: map[chat.Member][]Client{}, } // Make our own commands registry instance. @@ -75,7 +75,7 @@ func (h *Host) SetMotd(motd string) { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() - user := message.NewScreen(requestedName, term) + user := message.BufferedScreen(requestedName, term) client := h.addClient(user, term.Conn) defer h.removeClient(user, client) @@ -180,7 +180,7 @@ func (h *Host) Connect(term *sshd.Terminal) { logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) } -func (h *Host) addClient(user *message.User, conn sshd.Connection) *Client { +func (h *Host) addClient(user chat.Member, conn sshd.Connection) *Client { client := Client{ user: user, conn: conn, @@ -195,7 +195,7 @@ func (h *Host) addClient(user *message.User, conn sshd.Connection) *Client { return &client } -func (h *Host) removeClient(user *message.User, client *Client) { +func (h *Host) removeClient(user chat.Member, client *Client) { h.mu.Lock() defer h.mu.Unlock() @@ -212,7 +212,7 @@ func (h *Host) removeClient(user *message.User, client *Client) { } } -func (h *Host) findClients(user *message.User) []Client { +func (h *Host) findClients(user chat.Member) []Client { h.mu.Lock() defer h.mu.Unlock() return h.clients[user] @@ -244,7 +244,7 @@ func (h *Host) completeCommand(partial string) string { } // 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 Replier) 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 { return @@ -301,13 +301,12 @@ func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int, } // GetUser returns a message.User based on a name. -func (h *Host) GetUser(name string) (*message.User, bool) { +func (h *Host) GetUser(name string) (chat.Member, bool) { m, ok := h.MemberByID(name) if !ok { return nil, false } - u, ok := m.Member.(*message.User) - return u, ok + return m.Member, true } // InitCommands adds host-specific commands to a Commands container. These will @@ -337,7 +336,7 @@ func (h *Host) InitCommands(c *chat.Commands) { txt := fmt.Sprintf("[Sent PM to %s]", target.Name()) ms := message.NewSystemMsg(txt, msg.From()) room.Send(ms) - target.SetReplyTo(msg.From()) + target.(Replier).SetReplyTo(msg.From()) return nil }, }) @@ -353,7 +352,7 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("must specify message") } - target := msg.From().ReplyTo() + target := msg.From().(Replier).ReplyTo() if target == nil { return errors.New("no message to reply to") } @@ -392,7 +391,7 @@ func (h *Host) InitCommands(c *chat.Commands) { // FIXME: Handle many clients clients := h.findClients(target) var whois string - switch room.IsOp(msg.From()) { + switch room.IsOp(msg.From().(chat.Member)) { case true: whois = whoisAdmin(clients) case false: @@ -428,7 +427,7 @@ func (h *Host) InitCommands(c *chat.Commands) { PrefixHelp: "USER [DURATION]", Help: "Set USER as admin.", Handler: func(room *chat.Room, msg message.CommandMsg) error { - if !room.IsOp(msg.From()) { + if !room.IsOp(msg.From().(chat.Member)) { return errors.New("must be op") } @@ -471,7 +470,7 @@ func (h *Host) InitCommands(c *chat.Commands) { Help: "Ban USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { // TODO: Would be nice to specify what to ban. Key? Ip? etc. - if !room.IsOp(msg.From()) { + if !room.IsOp(msg.From().(chat.Member)) { return errors.New("must be op") } @@ -498,7 +497,7 @@ func (h *Host) InitCommands(c *chat.Commands) { body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) - target.Close() + target.(io.Closer).Close() logger.Debugf("Banned: \n-> %s", whoisAdmin(clients)) @@ -512,7 +511,7 @@ func (h *Host) InitCommands(c *chat.Commands) { PrefixHelp: "USER", Help: "Kick USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { - if !room.IsOp(msg.From()) { + if !room.IsOp(msg.From().(chat.Member)) { return errors.New("must be op") } @@ -528,7 +527,7 @@ func (h *Host) InitCommands(c *chat.Commands) { body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) - target.Close() + target.(io.Closer).Close() return nil }, }) @@ -550,7 +549,7 @@ func (h *Host) InitCommands(c *chat.Commands) { room.Send(message.NewSystemMsg(motd, user)) return nil } - if !room.IsOp(user) { + if !room.IsOp(user.(chat.Member)) { return errors.New("must be OP to modify the MOTD") } diff --git a/host_test.go b/host_test.go index 2e932776..187e7247 100644 --- a/host_test.go +++ b/host_test.go @@ -28,7 +28,7 @@ func stripPrompt(s string) string { func TestHostGetPrompt(t *testing.T) { var expected, actual string - u := message.NewUser("foo") + u := message.Screen("foo") actual = u.Prompt() expected = "[foo] " @@ -40,7 +40,7 @@ func TestHostGetPrompt(t *testing.T) { Theme: &message.Themes[0], }) actual = u.Prompt() - expected = "[\033[38;05;88mfoo\033[0m] " + expected = "[\033[38;05;1mfoo\033[0m] " if actual != expected { t.Errorf("Got: %q; Expected: %q", actual, expected) } diff --git a/whois.go b/whois.go index 7089667d..df595c9f 100644 --- a/whois.go +++ b/whois.go @@ -2,6 +2,7 @@ package sshchat import ( "net" + "time" humanize "github.com/dustin/go-humanize" "github.com/shazow/ssh-chat/chat/message" @@ -10,6 +11,10 @@ import ( // Helpers for printing whois messages +type joinTimestamped interface { + Joined() time.Time +} + func whoisPublic(clients []Client) string { // FIXME: Handle many clients conn, u := clients[0].conn, clients[0].user @@ -21,7 +26,7 @@ func whoisPublic(clients []Client) string { return "name: " + u.Name() + message.Newline + " > fingerprint: " + fingerprint + message.Newline + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + - " > joined: " + humanize.Time(u.Joined()) + " > joined: " + humanize.Time(u.(joinTimestamped).Joined()) } func whoisAdmin(clients []Client) string { @@ -37,5 +42,5 @@ func whoisAdmin(clients []Client) string { " > ip: " + ip + message.Newline + " > fingerprint: " + fingerprint + message.Newline + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + - " > joined: " + humanize.Time(u.Joined()) + " > joined: " + humanize.Time(u.(joinTimestamped).Joined()) } From d16b1f58299daab3fa2a96a96b1065eaf9be0194 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Tue, 13 Sep 2016 21:59:32 -0400 Subject: [PATCH 17/21] bugfix: Nil item --- host.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host.go b/host.go index 9feb022a..37900660 100644 --- a/host.go +++ b/host.go @@ -120,7 +120,7 @@ func (h *Host) Connect(term *sshd.Terminal) { // Should the user be op'd on join? if key := term.Conn.PublicKey(); key != nil { authItem, err := h.auth.ops.Get(newAuthKey(key)) - if err != nil { + if err == nil { err = h.Room.Ops.Add(set.Rename(authItem, member.ID())) } } From e86996e6b5dc22dadbf2dd948b880340cc9d7134 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 15 Sep 2016 14:01:15 -0400 Subject: [PATCH 18/21] progress: Host User interface, and more interfaces in general, tests pass --- chat/member.go | 8 ++++ chat/message/user.go | 4 ++ chat/room.go | 3 +- client.go | 30 ++++++++++----- host.go | 92 ++++++++++++++------------------------------ whois.go | 23 ++++------- 6 files changed, 71 insertions(+), 89 deletions(-) diff --git a/chat/member.go b/chat/member.go index 471cdd41..07839f75 100644 --- a/chat/member.go +++ b/chat/member.go @@ -1,6 +1,8 @@ package chat import ( + "time" + "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/set" ) @@ -21,4 +23,10 @@ type Member interface { SetConfig(message.UserConfig) Send(message.Message) error + + Joined() time.Time + ReplyTo() message.Author + SetReplyTo(message.Author) + Prompt() string + SetHighlight(string) error } diff --git a/chat/message/user.go b/chat/message/user.go index 022f577d..d16adffc 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -38,3 +38,7 @@ func (u *User) Color() int { func (u *User) ID() string { return SanitizeName(u.name) } + +func (u *User) Joined() time.Time { + return u.joined +} diff --git a/chat/room.go b/chat/room.go index 790770c3..975ff7f3 100644 --- a/chat/room.go +++ b/chat/room.go @@ -199,7 +199,8 @@ func (r *Room) MemberByID(id string) (*roomMember, bool) { if err != nil { return nil, false } - return m.Value().(*roomMember), true + rm, ok := m.Value().(*roomMember) + return rm, ok } // IsOp returns whether a user is an operator in this room. diff --git a/client.go b/client.go index d602ead5..54287198 100644 --- a/client.go +++ b/client.go @@ -1,21 +1,33 @@ package sshchat import ( - "time" + "sync" "github.com/shazow/ssh-chat/chat" - "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/sshd" ) -type Client struct { - user chat.Member - conn sshd.Connection +type client struct { + chat.Member + sync.Mutex + conns []sshd.Connection +} + +func (cl *client) Connections() []sshd.Connection { + return cl.conns +} - timestamp time.Time +func (cl *client) Close() error { + // TODO: Stack errors? + for _, conn := range cl.conns { + conn.Close() + } + return nil } -type Replier interface { - ReplyTo() message.Author - SetReplyTo(message.Author) +type User interface { + chat.Member + + Connections() []sshd.Connection + Close() error } diff --git a/host.go b/host.go index 37900660..8cf9b03b 100644 --- a/host.go +++ b/host.go @@ -35,7 +35,7 @@ type Host struct { mu sync.Mutex motd string count int - clients map[chat.Member][]Client + clients map[chat.Member][]client } // NewHost creates a Host on top of an existing listener. @@ -46,7 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host { listener: listener, commands: chat.Commands{}, auth: auth, - clients: map[chat.Member][]Client{}, + clients: map[chat.Member][]client{}, } // Make our own commands registry instance. @@ -75,10 +75,11 @@ func (h *Host) SetMotd(motd string) { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() - user := message.BufferedScreen(requestedName, term) - - client := h.addClient(user, term.Conn) - defer h.removeClient(user, client) + screen := message.BufferedScreen(requestedName, term) + user := &client{ + Member: screen, + conns: []sshd.Connection{term.Conn}, + } h.mu.Lock() motd := h.motd @@ -91,10 +92,10 @@ func (h *Host) Connect(term *sshd.Terminal) { user.SetConfig(cfg) // Close term once user is closed. - defer user.Close() + defer screen.Close() defer term.Close() - go user.Consume() + go screen.Consume() // Send MOTD if motd != "" { @@ -180,44 +181,6 @@ func (h *Host) Connect(term *sshd.Terminal) { logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) } -func (h *Host) addClient(user chat.Member, conn sshd.Connection) *Client { - client := Client{ - user: user, - conn: conn, - timestamp: time.Now(), - } - h.mu.Lock() - if _, ok := h.clients[user]; ok { - logger.Warningf("user collision: %q", user) - } - h.clients[user] = append(h.clients[user], client) - h.mu.Unlock() - return &client -} - -func (h *Host) removeClient(user chat.Member, client *Client) { - h.mu.Lock() - defer h.mu.Unlock() - - clients := h.clients[user] - for i, c := range clients { - // Find the user - if &c != client { - continue - } - // Delete corresponding client - clients[i] = clients[len(clients)-1] - clients = clients[:len(clients)-1] - break - } -} - -func (h *Host) findClients(user chat.Member) []Client { - h.mu.Lock() - defer h.mu.Unlock() - return h.clients[user] -} - // Serve our chat room onto the listener func (h *Host) Serve() { h.listener.HandlerFunc = h.Connect @@ -244,7 +207,7 @@ func (h *Host) completeCommand(partial string) string { } // AutoCompleteFunction returns a callback for terminal autocompletion -func (h *Host) AutoCompleteFunction(u Replier) 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) { if key != 9 { return @@ -301,12 +264,13 @@ func (h *Host) AutoCompleteFunction(u Replier) func(line string, pos int, key ru } // GetUser returns a message.User based on a name. -func (h *Host) GetUser(name string) (chat.Member, bool) { +func (h *Host) GetUser(name string) (User, bool) { m, ok := h.MemberByID(name) if !ok { return nil, false } - return m.Member, true + u, ok := m.Member.(User) + return u, ok } // InitCommands adds host-specific commands to a Commands container. These will @@ -336,7 +300,7 @@ func (h *Host) InitCommands(c *chat.Commands) { txt := fmt.Sprintf("[Sent PM to %s]", target.Name()) ms := message.NewSystemMsg(txt, msg.From()) room.Send(ms) - target.(Replier).SetReplyTo(msg.From()) + target.SetReplyTo(msg.From()) return nil }, }) @@ -352,7 +316,7 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("must specify message") } - target := msg.From().(Replier).ReplyTo() + target := msg.From().(chat.Member).ReplyTo() if target == nil { return errors.New("no message to reply to") } @@ -388,14 +352,12 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("user not found") } - // FIXME: Handle many clients - clients := h.findClients(target) var whois string switch room.IsOp(msg.From().(chat.Member)) { case true: - whois = whoisAdmin(clients) + whois = whoisAdmin(target) case false: - whois = whoisPublic(clients) + whois = whoisPublic(target) } room.Send(message.NewSystemMsg(whois, msg.From())) @@ -451,9 +413,12 @@ func (h *Host) InitCommands(c *chat.Commands) { room.Ops.Add(set.Keyize(user.ID())) } - for _, client := range h.findClients(user) { - h.auth.Op(client.conn.PublicKey(), until) - } + // 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)) @@ -489,17 +454,16 @@ func (h *Host) InitCommands(c *chat.Commands) { until, _ = time.ParseDuration(args[1]) } - clients := h.findClients(target) - for _, client := range clients { - h.auth.Ban(client.conn.PublicKey(), until) - h.auth.BanAddr(client.conn.RemoteAddr(), until) + for _, conn := range target.Connections() { + h.auth.Ban(conn.PublicKey(), until) + h.auth.BanAddr(conn.RemoteAddr(), until) } body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) - target.(io.Closer).Close() + target.Close() - logger.Debugf("Banned: \n-> %s", whoisAdmin(clients)) + logger.Debugf("Banned: \n-> %s", whoisAdmin(target)) return nil }, diff --git a/whois.go b/whois.go index df595c9f..af496cc2 100644 --- a/whois.go +++ b/whois.go @@ -2,7 +2,6 @@ package sshchat import ( "net" - "time" humanize "github.com/dustin/go-humanize" "github.com/shazow/ssh-chat/chat/message" @@ -11,28 +10,22 @@ import ( // Helpers for printing whois messages -type joinTimestamped interface { - Joined() time.Time -} - -func whoisPublic(clients []Client) string { - // FIXME: Handle many clients - conn, u := clients[0].conn, clients[0].user - +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.(joinTimestamped).Joined()) + " > joined: " + humanize.Time(u.Joined()) } -func whoisAdmin(clients []Client) string { - // FIXME: Handle many clients - conn, u := clients[0].conn, clients[0].user - +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 { @@ -42,5 +35,5 @@ func whoisAdmin(clients []Client) string { " > ip: " + ip + message.Newline + " > fingerprint: " + fingerprint + message.Newline + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + - " > joined: " + humanize.Time(u.(joinTimestamped).Joined()) + " > joined: " + humanize.Time(u.Joined()) } From 7f2b8e468990f9d90e23df160d0ab237d6ffae78 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 15 Sep 2016 15:00:50 -0400 Subject: [PATCH 19/21] refactor: Add message.Author.ID(), remove a bunch of type assertions --- chat/command.go | 10 +++++----- chat/member.go | 1 - chat/message/message.go | 1 + chat/message/screen.go | 4 ++-- chat/message/user.go | 14 +++++++------- chat/room.go | 6 +++--- host.go | 10 +++++----- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/chat/command.go b/chat/command.go index 1b518bae..ee3b9666 100644 --- a/chat/command.go +++ b/chat/command.go @@ -111,7 +111,7 @@ func InitCommands(c *Commands) { c.Add(Command{ Prefix: "/help", Handler: func(room *Room, msg message.CommandMsg) error { - op := room.IsOp(msg.From().(Member)) + op := room.IsOp(msg.From()) room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From())) return nil }, @@ -150,7 +150,7 @@ func InitCommands(c *Commands) { if len(args) != 1 { return ErrMissingArg } - member, ok := room.MemberByID(msg.From().(Member).ID()) + member, ok := room.MemberByID(msg.From().ID()) if !ok { return ErrMissingMember } @@ -253,7 +253,7 @@ func InitCommands(c *Commands) { PrefixHelp: "[USER]", Help: "Hide messages from USER, /unignore USER to stop hiding.", Handler: func(room *Room, msg message.CommandMsg) error { - from, ok := room.Member(msg.From().(Member)) + from, ok := room.Member(msg.From()) if !ok { return ErrMissingMember } @@ -278,7 +278,7 @@ func InitCommands(c *Commands) { return nil } - if id == msg.From().(Member).ID() { + if id == msg.From().ID() { return errors.New("cannot ignore self") } target, ok := room.MemberByID(id) @@ -302,7 +302,7 @@ func InitCommands(c *Commands) { Prefix: "/unignore", PrefixHelp: "USER", Handler: func(room *Room, msg message.CommandMsg) error { - from, ok := room.Member(msg.From().(Member)) + from, ok := room.Member(msg.From()) if !ok { return ErrMissingMember } diff --git a/chat/member.go b/chat/member.go index 07839f75..1ce6ffe4 100644 --- a/chat/member.go +++ b/chat/member.go @@ -16,7 +16,6 @@ type roomMember struct { type Member interface { message.Author - ID() string SetName(string) Config() message.UserConfig diff --git a/chat/message/message.go b/chat/message/message.go index 891e6ab2..e7cac397 100644 --- a/chat/message/message.go +++ b/chat/message/message.go @@ -7,6 +7,7 @@ import ( ) type Author interface { + ID() string Name() string Color() int } diff --git a/chat/message/screen.go b/chat/message/screen.go index 4027f03e..d0734697 100644 --- a/chat/message/screen.go +++ b/chat/message/screen.go @@ -39,7 +39,7 @@ func HandledScreen(name string, handler func(Message) error) *handledScreen { func Screen(name string) *baseScreen { return &baseScreen{ - User: NewUser(name), + user: NewUser(name), } } @@ -71,7 +71,7 @@ func (u *pipedScreen) Send(m Message) error { // User container that knows about writing to an IO screen. type baseScreen struct { sync.Mutex - *User + *user } func (u *baseScreen) Config() UserConfig { diff --git a/chat/message/user.go b/chat/message/user.go index d16adffc..30b23886 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -8,7 +8,7 @@ import ( const reHighlight = `\b(%s)\b` // User definition, implemented set Item interface and io.Writer -type User struct { +type user struct { joined time.Time name string @@ -16,8 +16,8 @@ type User struct { replyTo Author // Set when user gets a /msg, for replying. } -func NewUser(name string) *User { - u := User{ +func NewUser(name string) *user { + u := user{ name: name, config: DefaultUserConfig, joined: time.Now(), @@ -27,18 +27,18 @@ func NewUser(name string) *User { return &u } -func (u *User) Name() string { +func (u *user) Name() string { return u.name } -func (u *User) Color() int { +func (u *user) Color() int { return u.config.Seed } -func (u *User) ID() string { +func (u *user) ID() string { return SanitizeName(u.name) } -func (u *User) Joined() time.Time { +func (u *user) Joined() time.Time { return u.joined } diff --git a/chat/room.go b/chat/room.go index 975ff7f3..1b920d6b 100644 --- a/chat/room.go +++ b/chat/room.go @@ -92,7 +92,7 @@ func (r *Room) HandleMsg(m message.Message) { roomMember := item.Value().(*roomMember) user := roomMember.Member - if fromMsg != nil && fromMsg.From() != nil && roomMember.Ignored.In(fromMsg.From().(Member).ID()) { + if fromMsg != nil && fromMsg.From() != nil && roomMember.Ignored.In(fromMsg.From().ID()) { // Skip because ignored return } @@ -182,7 +182,7 @@ func (r *Room) Rename(oldID string, u Member) error { // Member returns a corresponding Member object to a User if the Member is // present in this room. -func (r *Room) Member(u Member) (*roomMember, bool) { +func (r *Room) Member(u message.Author) (*roomMember, bool) { m, ok := r.MemberByID(u.ID()) if !ok { return nil, false @@ -204,7 +204,7 @@ func (r *Room) MemberByID(id string) (*roomMember, bool) { } // IsOp returns whether a user is an operator in this room. -func (r *Room) IsOp(u Member) bool { +func (r *Room) IsOp(u message.Author) bool { return r.Ops.In(u.ID()) } diff --git a/host.go b/host.go index 8cf9b03b..bb35505f 100644 --- a/host.go +++ b/host.go @@ -353,7 +353,7 @@ func (h *Host) InitCommands(c *chat.Commands) { } var whois string - switch room.IsOp(msg.From().(chat.Member)) { + switch room.IsOp(msg.From()) { case true: whois = whoisAdmin(target) case false: @@ -389,7 +389,7 @@ func (h *Host) InitCommands(c *chat.Commands) { PrefixHelp: "USER [DURATION]", Help: "Set USER as admin.", Handler: func(room *chat.Room, msg message.CommandMsg) error { - if !room.IsOp(msg.From().(chat.Member)) { + if !room.IsOp(msg.From()) { return errors.New("must be op") } @@ -435,7 +435,7 @@ func (h *Host) InitCommands(c *chat.Commands) { Help: "Ban USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { // TODO: Would be nice to specify what to ban. Key? Ip? etc. - if !room.IsOp(msg.From().(chat.Member)) { + if !room.IsOp(msg.From()) { return errors.New("must be op") } @@ -475,7 +475,7 @@ func (h *Host) InitCommands(c *chat.Commands) { PrefixHelp: "USER", Help: "Kick USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { - if !room.IsOp(msg.From().(chat.Member)) { + if !room.IsOp(msg.From()) { return errors.New("must be op") } @@ -513,7 +513,7 @@ func (h *Host) InitCommands(c *chat.Commands) { room.Send(message.NewSystemMsg(motd, user)) return nil } - if !room.IsOp(user.(chat.Member)) { + if !room.IsOp(user) { return errors.New("must be OP to modify the MOTD") } From cd5686e20e66476b7d4401c2e0afbeaa32234f8b Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 16 Sep 2016 12:31:02 -0400 Subject: [PATCH 20/21] refactor: Move extraneous User interfaces from chat to host --- chat/member.go | 10 +--------- client.go | 16 ++++++++++++++-- host.go | 6 +++--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/chat/member.go b/chat/member.go index 1ce6ffe4..1c27ee6b 100644 --- a/chat/member.go +++ b/chat/member.go @@ -1,8 +1,6 @@ package chat import ( - "time" - "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/set" ) @@ -16,16 +14,10 @@ type roomMember struct { type Member interface { message.Author - SetName(string) - Config() message.UserConfig SetConfig(message.UserConfig) Send(message.Message) error - Joined() time.Time - ReplyTo() message.Author - SetReplyTo(message.Author) - Prompt() string - SetHighlight(string) error + SetName(string) } diff --git a/client.go b/client.go index 54287198..b43d5712 100644 --- a/client.go +++ b/client.go @@ -2,13 +2,15 @@ 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 { - chat.Member + UserMember sync.Mutex conns []sshd.Connection } @@ -25,9 +27,19 @@ func (cl *client) Close() error { return nil } -type User interface { +type UserMember interface { chat.Member + Joined() time.Time + Prompt() string + ReplyTo() message.Author + SetHighlight(string) error + SetReplyTo(message.Author) +} + +type User interface { + UserMember + Connections() []sshd.Connection Close() error } diff --git a/host.go b/host.go index bb35505f..4bf97b2c 100644 --- a/host.go +++ b/host.go @@ -77,8 +77,8 @@ func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() screen := message.BufferedScreen(requestedName, term) user := &client{ - Member: screen, - conns: []sshd.Connection{term.Conn}, + UserMember: screen, + conns: []sshd.Connection{term.Conn}, } h.mu.Lock() @@ -316,7 +316,7 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("must specify message") } - target := msg.From().(chat.Member).ReplyTo() + target := msg.From().(UserMember).ReplyTo() if target == nil { return errors.New("no message to reply to") } From 7b3818acc1cf13cf9fce74bc51a7997482cf8ac8 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sat, 17 Sep 2016 11:27:11 -0400 Subject: [PATCH 21/21] refactor: host.UserMember{chat.Member} -> host.Member{chat.Member} --- client.go | 6 +++--- host.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client.go b/client.go index b43d5712..996171a8 100644 --- a/client.go +++ b/client.go @@ -10,7 +10,7 @@ import ( ) type client struct { - UserMember + Member sync.Mutex conns []sshd.Connection } @@ -27,7 +27,7 @@ func (cl *client) Close() error { return nil } -type UserMember interface { +type Member interface { chat.Member Joined() time.Time @@ -38,7 +38,7 @@ type UserMember interface { } type User interface { - UserMember + Member Connections() []sshd.Connection Close() error diff --git a/host.go b/host.go index 4bf97b2c..9c43599c 100644 --- a/host.go +++ b/host.go @@ -77,8 +77,8 @@ func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() screen := message.BufferedScreen(requestedName, term) user := &client{ - UserMember: screen, - conns: []sshd.Connection{term.Conn}, + Member: screen, + conns: []sshd.Connection{term.Conn}, } h.mu.Lock() @@ -316,7 +316,7 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("must specify message") } - target := msg.From().(UserMember).ReplyTo() + target := msg.From().(Member).ReplyTo() if target == nil { return errors.New("no message to reply to") }