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

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,26 @@
package message
// Identifier is an interface that can uniquely identify itself.
type Identifier interface {
Id() string
SetId(string)
Name() string
}
// SimpleId is a simple Identifier implementation used for testing.
type SimpleId string
// Id returns the Id as a string.
func (i SimpleId) Id() string {
return string(i)
}
// SetId is a no-op
func (i SimpleId) SetId(s string) {
// no-op
}
// Name returns the Id
func (i SimpleId) Name() string {
return i.Id()
}

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

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

View File

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

View File

@ -1,19 +1,7 @@
package chat
package message
import "testing"
type testId string
func (i testId) Id() string {
return string(i)
}
func (i testId) SetId(s string) {
// no-op
}
func (i testId) Name() string {
return i.Id()
}
func TestMessage(t *testing.T) {
var expected, actual string
@ -23,7 +11,7 @@ func TestMessage(t *testing.T) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
u := NewUser(testId("foo"))
u := NewUser(SimpleId("foo"))
expected = "foo: hello"
actual = NewPublicMsg("hello", u).String()
if actual != expected {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3
cmd.go
View File

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

59
host.go
View File

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

View File

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

View File

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