diff --git a/chat/channel.go b/chat/channel.go index 34d981f..4fadb69 100644 --- a/chat/channel.go +++ b/chat/channel.go @@ -142,12 +142,20 @@ func (ch *Channel) Leave(u *User) error { // Member returns a corresponding Member object to a User if the Member is // present in this channel. func (ch *Channel) Member(u *User) (*Member, bool) { - m, err := ch.members.Get(u.Id()) - if err != nil { + m, ok := ch.MemberById(u.Id()) + if !ok { return nil, false } // Check that it's the same user - if m.(*Member).User != u { + if m.User != u { + return nil, false + } + return m, true +} + +func (ch *Channel) MemberById(id Id) (*Member, bool) { + m, err := ch.members.Get(id) + if err != nil { return nil, false } return m.(*Member), true diff --git a/chat/command.go b/chat/command.go index 1328c49..351d090 100644 --- a/chat/command.go +++ b/chat/command.go @@ -86,7 +86,7 @@ func (c Commands) Help(showOp bool) string { } help := "Available commands:" + Newline + NewCommandsHelp(normal).String() if showOp { - help += Newline + "Operator commands:" + Newline + NewCommandsHelp(op).String() + help += Newline + "-> Operator commands:" + Newline + NewCommandsHelp(op).String() } return help } @@ -231,12 +231,12 @@ func InitCommands(c *Commands) { // TODO: Add support for fingerprint-based op'ing. This will // probably need to live in host land. - member, err := channel.members.Get(Id(args[0])) - if err != nil { + member, ok := channel.MemberById(Id(args[0])) + if !ok { return errors.New("user not found") } - member.(*Member).Op = true + member.Op = true return nil }, }) diff --git a/chat/user.go b/chat/user.go index 203a48a..75ea330 100644 --- a/chat/user.go +++ b/chat/user.go @@ -20,6 +20,7 @@ type User struct { joined time.Time msg chan Message done chan struct{} + replyTo *User // Set when user gets a /msg, for replying. closed bool closeOnce sync.Once } diff --git a/command.go b/command.go new file mode 100644 index 0000000..4700054 --- /dev/null +++ b/command.go @@ -0,0 +1,39 @@ +package main + +import ( + "errors" + + "github.com/shazow/ssh-chat/chat" +) + +// InitCommands adds host-specific commands to a Commands container. +func InitCommands(h *Host, c *chat.Commands) { + c.Add(chat.Command{ + Op: true, + Prefix: "/msg", + PrefixHelp: "USER MESSAGE", + Help: "Send MESSAGE to USER.", + Handler: func(channel *chat.Channel, msg chat.CommandMsg) error { + if !channel.IsOp(msg.From()) { + return errors.New("must be op") + } + + args := msg.Args() + switch len(args) { + case 0: + return errors.New("must specify user") + case 1: + return errors.New("must specify message") + } + + member, ok := channel.MemberById(chat.Id(args[0])) + if !ok { + return errors.New("user not found") + } + + m := chat.NewPrivateMsg("hello", msg.From(), member.User) + channel.Send(m) + return nil + }, + }) +} diff --git a/host.go b/host.go index 51a1fb5..d3510e7 100644 --- a/host.go +++ b/host.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io" "strings" @@ -9,6 +10,15 @@ import ( "github.com/shazow/ssh-chat/sshd" ) +// GetPrompt will render the terminal prompt string based on the user. +func GetPrompt(user *chat.User) string { + name := user.Name() + if user.Config.Theme != nil { + name = user.Config.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 channels, if we want. type Host struct { @@ -35,6 +45,7 @@ func NewHost(listener *sshd.SSHListener) *Host { // Make our own commands registry instance. commands := chat.Commands{} chat.InitCommands(&commands) + h.InitCommands(&commands) ch.SetCommands(commands) go ch.Serve() @@ -159,11 +170,58 @@ func (h *Host) AutoCompleteFunction(line string, pos int, key rune) (newLine str return } -// GetPrompt will render the terminal prompt string based on the user. -func GetPrompt(user *chat.User) string { - name := user.Name() - if user.Config.Theme != nil { - name = user.Config.Theme.ColorName(user) - } - return fmt.Sprintf("[%s] ", name) +// InitCommands adds host-specific commands to a Commands container. These will +// override any existing commands. +func (h *Host) InitCommands(c *chat.Commands) { + c.Add(chat.Command{ + Prefix: "/msg", + PrefixHelp: "USER MESSAGE", + Help: "Send MESSAGE to USER.", + Handler: func(channel *chat.Channel, msg chat.CommandMsg) error { + args := msg.Args() + switch len(args) { + case 0: + return errors.New("must specify user") + case 1: + return errors.New("must specify message") + } + + member, ok := channel.MemberById(chat.Id(args[0])) + if !ok { + return errors.New("user not found") + } + + m := chat.NewPrivateMsg(strings.Join(args[2:], " "), msg.From(), member.User) + channel.Send(m) + return nil + }, + }) + + // Op commands + c.Add(chat.Command{ + Op: true, + Prefix: "/kick", + PrefixHelp: "USER", + Help: "Kick USER from the server.", + Handler: func(channel *chat.Channel, msg chat.CommandMsg) error { + if !channel.IsOp(msg.From()) { + return errors.New("must be op") + } + + args := msg.Args() + if len(args) == 0 { + return errors.New("must specify user") + } + + member, ok := channel.MemberById(chat.Id(args[0])) + if !ok { + return errors.New("user not found") + } + + body := fmt.Sprintf("%s was kicked by %s.", member.Name(), msg.From().Name()) + channel.Send(chat.NewAnnounceMsg(body)) + member.User.Close() + return nil + }, + }) } diff --git a/sshd/multi.go b/sshd/multi.go deleted file mode 100644 index 62447a7..0000000 --- a/sshd/multi.go +++ /dev/null @@ -1,42 +0,0 @@ -package sshd - -import ( - "fmt" - "io" - "strings" -) - -// Keep track of multiple errors and coerce them into one error -type MultiError []error - -func (e MultiError) Error() string { - switch len(e) { - case 0: - return "" - case 1: - return e[0].Error() - default: - errs := []string{} - for _, err := range e { - errs = append(errs, err.Error()) - } - return fmt.Sprintf("%d errors: %s", strings.Join(errs, "; ")) - } -} - -// Keep track of multiple closers and close them all as one closer -type MultiCloser []io.Closer - -func (c MultiCloser) Close() error { - errors := MultiError{} - for _, closer := range c { - err := closer.Close() - if err != nil { - errors = append(errors, err) - } - } - if len(errors) == 0 { - return nil - } - return errors -} diff --git a/sshd/pty.go b/sshd/pty.go index 5aecc3e..06d34f0 100644 --- a/sshd/pty.go +++ b/sshd/pty.go @@ -1,8 +1,9 @@ -// Borrowed from go.crypto circa 2011 package sshd import "encoding/binary" +// Helpers below are borrowed from go.crypto circa 2011: + // parsePtyRequest parses the payload of the pty-req message and extracts the // dimensions of the terminal. See RFC 4254, section 6.2. func parsePtyRequest(s []byte) (width, height int, ok bool) {