Split up message types, added exit codes, basic command handling.

This commit is contained in:
Andrey Petrov 2014-12-25 16:25:02 -08:00
parent 3bb4bbf991
commit dac5cfbb5e
8 changed files with 272 additions and 105 deletions

View File

@ -1,50 +1,39 @@
package chat
import "fmt"
import (
"errors"
"fmt"
)
const historyLen = 20
const channelBuffer = 10
var ErrChannelClosed = errors.New("channel closed")
// Channel definition, also a Set of User Items
type Channel struct {
topic string
history *History
users *Set
broadcast chan Message
commands Commands
closed bool
}
// Create new channel and start broadcasting goroutine.
func NewChannel() *Channel {
broadcast := make(chan Message, channelBuffer)
ch := Channel{
return &Channel{
broadcast: broadcast,
history: NewHistory(historyLen),
users: NewSet(),
commands: defaultCmdHandlers,
}
go func() {
for m := range broadcast {
// TODO: Handle commands etc?
ch.users.Each(func(u Item) {
user := u.(*User)
if m.from == user {
// Skip
return
}
err := user.Send(m)
if err != nil {
ch.Leave(user)
user.Close()
}
})
}
}()
return &ch
}
func (ch *Channel) Close() {
ch.closed = true
ch.users.Each(func(u Item) {
u.(*User).Close()
})
@ -52,17 +41,63 @@ func (ch *Channel) Close() {
close(ch.broadcast)
}
// Handle a message, will block until done.
func (ch *Channel) handleMsg(m Message) {
switch m.(type) {
case CommandMsg:
cmd := m.(CommandMsg)
err := ch.commands.Run(cmd)
if err != nil {
m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
go ch.handleMsg(m)
}
case MessageTo:
user := m.(MessageTo).To()
user.Send(m)
default:
fromMsg, skip := m.(MessageFrom)
var skipUser *User
if skip {
skipUser = fromMsg.From()
}
ch.users.Each(func(u Item) {
user := u.(*User)
if skip && skipUser == user {
// Skip
return
}
err := user.Send(m)
if err != nil {
ch.Leave(user)
user.Close()
}
})
}
}
// Serve will consume the broadcast channel and handle the messages, should be
// run in a goroutine.
func (ch *Channel) Serve() {
for m := range ch.broadcast {
go ch.handleMsg(m)
}
}
func (ch *Channel) Send(m Message) {
ch.broadcast <- m
}
func (ch *Channel) Join(u *User) error {
if ch.closed {
return ErrChannelClosed
}
err := ch.users.Add(u)
if err != nil {
return err
}
s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), ch.users.Len())
ch.Send(*NewMessage(s))
ch.Send(NewAnnounceMsg(s))
return nil
}
@ -72,7 +107,7 @@ func (ch *Channel) Leave(u *User) error {
return err
}
s := fmt.Sprintf("%s left.", u.Name())
ch.Send(*NewMessage(s))
ch.Send(NewAnnounceMsg(s))
return nil
}

View File

@ -12,25 +12,28 @@ func TestChannel(t *testing.T) {
u := NewUser("foo")
ch := NewChannel()
go ch.Serve()
defer ch.Close()
err := ch.Join(u)
if err != nil {
t.Error(err)
t.Fatal(err)
}
u.ConsumeOne(s)
expected = []byte(" * foo joined. (Connected: 1)")
expected = []byte(" * foo joined. (Connected: 1)" + Newline)
s.Read(&actual)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
// XXX
t.Skip()
m := NewMessage("hello").From(u)
ch.Send(*m)
m := NewPublicMsg("hello", u)
ch.Send(m)
u.ConsumeOne(s)
expected = []byte("foo: hello")
expected = []byte("foo: hello" + Newline)
s.Read(&actual)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)

View File

@ -5,31 +5,30 @@ import (
"strings"
)
var ErrInvalidCommand error = errors.New("invalid command")
var ErrNoOwner error = errors.New("command without owner")
var ErrInvalidCommand = errors.New("invalid command")
var ErrNoOwner = errors.New("command without owner")
type CmdHandler func(msg Message, args []string) error
type CommandHandler func(c CommandMsg) error
type Commands map[string]CmdHandler
type Commands map[string]CommandHandler
// Register command
func (c Commands) Add(cmd string, handler CmdHandler) {
c[cmd] = handler
func (c Commands) Add(command string, handler CommandHandler) {
c[command] = handler
}
// Execute command message, assumes IsCommand was checked
func (c Commands) Run(msg Message) error {
func (c Commands) Run(msg CommandMsg) error {
if msg.from == nil {
return ErrNoOwner
}
cmd, args := msg.ParseCommand()
handler, ok := c[cmd]
handler, ok := c[msg.Command()]
if !ok {
return ErrInvalidCommand
}
return handler(msg, args)
return handler(msg)
}
var defaultCmdHandlers Commands
@ -37,8 +36,8 @@ var defaultCmdHandlers Commands
func init() {
c := Commands{}
c.Add("me", func(msg Message, args []string) error {
me := strings.TrimLeft(msg.Body, "/me")
c.Add("/me", func(msg CommandMsg) error {
me := strings.TrimLeft(msg.body, "/me")
if me == "" {
me = " is at a loss for words."
}

View File

@ -6,74 +6,197 @@ import (
"time"
)
// Container for messages sent to chat
type Message struct {
Body string
from *User // Not set for Sys messages
to *User // Only set for PMs
channel *Channel // Not set for global commands
timestamp time.Time
themeCache *map[*Theme]string
// Message is an interface for messages.
type Message interface {
Render(*Theme) string
String() string
}
func NewMessage(body string) *Message {
m := Message{
Body: body,
timestamp: time.Now(),
type MessageTo interface {
Message
To() *User
}
type MessageFrom interface {
Message
From() *User
}
func ParseInput(body string, from *User) Message {
m := NewPublicMsg(body, from)
cmd, isCmd := m.ParseCommand()
if isCmd {
return cmd
}
return &m
}
// Set message recipient
func (m *Message) To(u *User) *Message {
m.to = u
return m
}
// Set message sender
func (m *Message) From(u *User) *Message {
m.from = u
return m
// Msg is a base type for other message types.
type Msg struct {
Message
body string
timestamp time.Time
// TODO: themeCache *map[*Theme]string
}
// Set channel
func (m *Message) Channel(ch *Channel) *Message {
m.channel = ch
return m
}
// Render message based on the given theme
func (m *Message) Render(*Theme) string {
// TODO: Return []byte?
// Render message based on a theme.
func (m *Msg) Render(t *Theme) string {
// TODO: Render based on theme
// TODO: Cache based on theme
var msg string
if m.to != nil && m.from != nil {
msg = fmt.Sprintf("[PM from %s] %s", m.from.Name(), m.Body)
} else if m.from != nil {
msg = fmt.Sprintf("%s: %s", m.from.Name(), m.Body)
} else if m.to != nil {
msg = fmt.Sprintf("-> %s", m.Body)
} else {
msg = fmt.Sprintf(" * %s", m.Body)
}
return msg
return m.body
}
// Render message without a theme
func (m *Message) String() string {
func (m *Msg) String() string {
return m.Render(nil)
}
// Wether message is a command (starts with /)
func (m *Message) IsCommand() bool {
return strings.HasPrefix(m.Body, "/")
// PublicMsg is any message from a user sent to the channel.
type PublicMsg struct {
Msg
from *User
}
// Parse command (assumes IsCommand was already called)
func (m *Message) ParseCommand() (string, []string) {
// TODO: Tokenize this properly, to support quoted args?
cmd := strings.Split(m.Body, " ")
args := cmd[1:]
return cmd[0][1:], args
func NewPublicMsg(body string, from *User) *PublicMsg {
return &PublicMsg{
Msg: Msg{
body: body,
timestamp: time.Now(),
},
from: from,
}
}
func (m *PublicMsg) From() *User {
return m.from
}
func (m *PublicMsg) ParseCommand() (*CommandMsg, bool) {
// Check if the message is a command
if !strings.HasPrefix(m.body, "/") {
return nil, false
}
// Parse
// TODO: Handle quoted fields properly
fields := strings.Fields(m.body)
command, args := fields[0], fields[1:]
msg := CommandMsg{
PublicMsg: m,
command: command,
args: args,
}
return &msg, true
}
func (m *PublicMsg) Render(t *Theme) string {
return fmt.Sprintf("%s: %s", m.from.Name(), m.body)
}
func (m *PublicMsg) String() string {
return m.Render(nil)
}
// EmoteMsg is a /me message sent to the channel.
type EmoteMsg struct {
PublicMsg
}
func (m *EmoteMsg) Render(t *Theme) string {
return fmt.Sprintf("** %s %s", m.from.Name(), m.body)
}
func (m *EmoteMsg) String() string {
return m.Render(nil)
}
// PrivateMsg is a message sent to another user, not shown to anyone else.
type PrivateMsg struct {
PublicMsg
to *User
}
func NewPrivateMsg(body string, from *User, to *User) *PrivateMsg {
return &PrivateMsg{
PublicMsg: *NewPublicMsg(body, from),
to: to,
}
}
func (m *PrivateMsg) To() *User {
return m.to
}
func (m *PrivateMsg) Render(t *Theme) string {
return fmt.Sprintf("[PM from %s] %s", m.from.Name(), m.body)
}
func (m *PrivateMsg) String() string {
return m.Render(nil)
}
// SystemMsg is a response sent from the server directly to a user, not shown
// to anyone else. Usually in response to something, like /help.
type SystemMsg struct {
Msg
to *User
}
func NewSystemMsg(body string, to *User) *SystemMsg {
return &SystemMsg{
Msg: Msg{
body: body,
timestamp: time.Now(),
},
to: to,
}
}
func (m *SystemMsg) Render(t *Theme) string {
return fmt.Sprintf("-> %s", m.body)
}
func (m *SystemMsg) String() string {
return m.Render(nil)
}
func (m *SystemMsg) To() *User {
return m.to
}
// AnnounceMsg is a message sent from the server to everyone, like a join or
// leave event.
type AnnounceMsg struct {
Msg
}
func NewAnnounceMsg(body string) *AnnounceMsg {
return &AnnounceMsg{
Msg: Msg{
body: body,
timestamp: time.Now(),
},
}
}
func (m *AnnounceMsg) Render(t *Theme) string {
return fmt.Sprintf(" * %s", m.body)
}
func (m *AnnounceMsg) String() string {
return m.Render(nil)
}
type CommandMsg struct {
*PublicMsg
command string
args []string
channel *Channel
}
func (m *CommandMsg) Command() string {
return m.command
}
func (m *CommandMsg) Args() []string {
return m.args
}

View File

@ -6,26 +6,26 @@ func TestMessage(t *testing.T) {
var expected, actual string
expected = " * foo"
actual = NewMessage("foo").String()
actual = NewAnnounceMsg("foo").String()
if actual != expected {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
u := NewUser("foo")
expected = "foo: hello"
actual = NewMessage("hello").From(u).String()
actual = NewPublicMsg("hello", u).String()
if actual != expected {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
expected = "-> hello"
actual = NewMessage("hello").To(u).String()
actual = NewSystemMsg("hello", u).String()
if actual != expected {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
expected = "[PM from foo] hello"
actual = NewMessage("hello").From(u).To(u).String()
actual = NewPrivateMsg("hello", u, u).String()
if actual != expected {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}

View File

@ -10,14 +10,14 @@ func TestMakeUser(t *testing.T) {
s := &MockScreen{}
u := NewUser("foo")
m := NewMessage("hello")
m := NewAnnounceMsg("hello")
defer u.Close()
u.Send(*m)
u.Send(m)
u.ConsumeOne(s)
s.Read(&actual)
expected = []byte(m.String())
expected = []byte(m.String() + Newline)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}

7
cmd.go
View File

@ -46,6 +46,7 @@ func main() {
if p == nil {
fmt.Print(err)
}
os.Exit(1)
return
}
@ -81,12 +82,14 @@ func main() {
privateKey, err := ioutil.ReadFile(privateKeyPath)
if err != nil {
logger.Errorf("Failed to load identity: %v", err)
os.Exit(2)
return
}
signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil {
logger.Errorf("Failed to prase key: %v", err)
logger.Errorf("Failed to parse key: %v", err)
os.Exit(3)
return
}
@ -97,6 +100,7 @@ func main() {
s, err := sshd.ListenSSH(options.Bind, config)
if err != nil {
logger.Errorf("Failed to listen on socket: %v", err)
os.Exit(4)
return
}
defer s.Close()
@ -143,4 +147,5 @@ func main() {
<-sig // Wait for ^C signal
logger.Warningf("Interrupt signal detected, shutting down.")
os.Exit(0)
}

View File

@ -18,10 +18,12 @@ type Host struct {
// NewHost creates a Host on top of an existing listener
func NewHost(listener *sshd.SSHListener) *Host {
ch := chat.NewChannel()
h := Host{
listener: listener,
channel: chat.NewChannel(),
channel: ch,
}
go ch.Serve()
return &h
}
@ -51,8 +53,8 @@ func (h *Host) Connect(term *sshd.Terminal) {
logger.Errorf("Terminal reading error: %s", err)
break
}
m := chat.NewMessage(line).From(user)
h.channel.Send(*m)
m := chat.ParseInput(line, user)
h.channel.Send(m)
}
err = h.channel.Leave(user)