mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-06-02 00:21:04 +03:00
Added /nick and /exit commands.
This commit is contained in:
parent
325f8921da
commit
b40136c3e1
@ -30,7 +30,7 @@ func NewChannel() *Channel {
|
|||||||
broadcast: broadcast,
|
broadcast: broadcast,
|
||||||
history: NewHistory(historyLen),
|
history: NewHistory(historyLen),
|
||||||
users: NewSet(),
|
users: NewSet(),
|
||||||
commands: defaultCmdHandlers,
|
commands: *defaultCmdHandlers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,15 +47,15 @@ func (ch *Channel) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle a message, will block until done.
|
// Handle a message, will block until done.
|
||||||
func (ch *Channel) handleMsg(m Message) {
|
func (ch *Channel) HandleMsg(m Message) {
|
||||||
logger.Printf("ch.handleMsg(%v)", m)
|
logger.Printf("ch.HandleMsg(%v)", m)
|
||||||
switch m := m.(type) {
|
switch m := m.(type) {
|
||||||
case *CommandMsg:
|
case *CommandMsg:
|
||||||
cmd := *m
|
cmd := *m
|
||||||
err := ch.commands.Run(ch, cmd)
|
err := ch.commands.Run(ch, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
|
m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
|
||||||
go ch.handleMsg(m)
|
go ch.HandleMsg(m)
|
||||||
}
|
}
|
||||||
case MessageTo:
|
case MessageTo:
|
||||||
user := m.To()
|
user := m.To()
|
||||||
@ -86,7 +86,7 @@ func (ch *Channel) handleMsg(m Message) {
|
|||||||
// run in a goroutine.
|
// run in a goroutine.
|
||||||
func (ch *Channel) Serve() {
|
func (ch *Channel) Serve() {
|
||||||
for m := range ch.broadcast {
|
for m := range ch.broadcast {
|
||||||
go ch.handleMsg(m)
|
go ch.HandleMsg(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,28 +2,66 @@ package chat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidCommand = errors.New("invalid command")
|
var ErrInvalidCommand = errors.New("invalid command")
|
||||||
var ErrNoOwner = errors.New("command without owner")
|
var ErrNoOwner = errors.New("command without owner")
|
||||||
|
var ErrMissingArg = errors.New("missing argument")
|
||||||
|
|
||||||
type CommandHandler func(*Channel, CommandMsg) error
|
type CommandHandler func(*Channel, CommandMsg) error
|
||||||
|
|
||||||
type Commands map[string]CommandHandler
|
type Commands struct {
|
||||||
|
handlers map[string]CommandHandler
|
||||||
// Register command
|
help map[string]string
|
||||||
func (c Commands) Add(command string, handler CommandHandler) {
|
sync.RWMutex
|
||||||
c[command] = handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute command message, assumes IsCommand was checked
|
func NewCommands() *Commands {
|
||||||
|
return &Commands{
|
||||||
|
handlers: map[string]CommandHandler{},
|
||||||
|
help: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register command. If help string is empty, it will be hidden from Help().
|
||||||
|
func (c Commands) Add(command string, help string, handler CommandHandler) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
c.handlers[command] = handler
|
||||||
|
|
||||||
|
if help != "" {
|
||||||
|
c.help[command] = help
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias will add another command for the same handler, won't get added to help.
|
||||||
|
func (c Commands) Alias(command string, alias string) error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
handler, ok := c.handlers[command]
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidCommand
|
||||||
|
}
|
||||||
|
c.handlers[alias] = handler
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute command message, assumes IsCommand was checked.
|
||||||
func (c Commands) Run(channel *Channel, msg CommandMsg) error {
|
func (c Commands) Run(channel *Channel, msg CommandMsg) error {
|
||||||
if msg.from == nil {
|
if msg.from == nil {
|
||||||
return ErrNoOwner
|
return ErrNoOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, ok := c[msg.Command()]
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
handler, ok := c.handlers[msg.Command()]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
}
|
}
|
||||||
@ -31,12 +69,26 @@ func (c Commands) Run(channel *Channel, msg CommandMsg) error {
|
|||||||
return handler(channel, msg)
|
return handler(channel, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultCmdHandlers Commands
|
// Help will return collated help text as one string.
|
||||||
|
func (c Commands) Help() string {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
|
r := make([]string, len(c.help))
|
||||||
|
for cmd, line := range c.help {
|
||||||
|
r = append(r, fmt.Sprintf("%s %s", cmd, line))
|
||||||
|
}
|
||||||
|
sort.Strings(r)
|
||||||
|
|
||||||
|
return strings.Join(r, Newline)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultCmdHandlers *Commands
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
c := Commands{}
|
c := NewCommands()
|
||||||
|
|
||||||
c.Add("/me", func(channel *Channel, msg CommandMsg) error {
|
c.Add("/me", "", func(channel *Channel, msg CommandMsg) error {
|
||||||
me := strings.TrimLeft(msg.body, "/me")
|
me := strings.TrimLeft(msg.body, "/me")
|
||||||
if me == "" {
|
if me == "" {
|
||||||
me = " is at a loss for words."
|
me = " is at a loss for words."
|
||||||
@ -48,5 +100,24 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
c.Add("/exit", "Exit the chat.", func(channel *Channel, msg CommandMsg) error {
|
||||||
|
msg.From().Close()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
c.Alias("/exit", "/quit")
|
||||||
|
|
||||||
|
c.Add("/nick", "NAME\tRename yourself.", func(channel *Channel, msg CommandMsg) error {
|
||||||
|
args := msg.Args()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return ErrMissingArg
|
||||||
|
}
|
||||||
|
u := msg.From()
|
||||||
|
oldName := u.Name()
|
||||||
|
u.SetName(args[0])
|
||||||
|
|
||||||
|
channel.Send(NewAnnounceMsg(fmt.Sprintf("%s is now known as %s.", oldName, u.Name())))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
defaultCmdHandlers = c
|
defaultCmdHandlers = c
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
type Message interface {
|
type Message interface {
|
||||||
Render(*Theme) string
|
Render(*Theme) string
|
||||||
String() string
|
String() string
|
||||||
|
Command() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageTo interface {
|
type MessageTo interface {
|
||||||
@ -50,6 +51,10 @@ func (m *Msg) String() string {
|
|||||||
return m.Render(nil)
|
return m.Render(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Msg) Command() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// PublicMsg is any message from a user sent to the channel.
|
// PublicMsg is any message from a user sent to the channel.
|
||||||
type PublicMsg struct {
|
type PublicMsg struct {
|
||||||
Msg
|
Msg
|
||||||
|
32
chat/user.go
32
chat/user.go
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,14 +14,15 @@ var ErrUserClosed = errors.New("user closed")
|
|||||||
|
|
||||||
// User definition, implemented set Item interface and io.Writer
|
// User definition, implemented set Item interface and io.Writer
|
||||||
type User struct {
|
type User struct {
|
||||||
name string
|
Config UserConfig
|
||||||
op bool
|
name string
|
||||||
colorIdx int
|
op bool
|
||||||
joined time.Time
|
colorIdx int
|
||||||
msg chan Message
|
joined time.Time
|
||||||
done chan struct{}
|
msg chan Message
|
||||||
closed bool
|
done chan struct{}
|
||||||
Config UserConfig
|
closed bool
|
||||||
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(name string) *User {
|
func NewUser(name string) *User {
|
||||||
@ -75,9 +77,11 @@ func (u *User) Wait() {
|
|||||||
|
|
||||||
// Disconnect user, stop accepting messages
|
// Disconnect user, stop accepting messages
|
||||||
func (u *User) Close() {
|
func (u *User) Close() {
|
||||||
u.closed = true
|
u.closeOnce.Do(func() {
|
||||||
close(u.done)
|
u.closed = true
|
||||||
close(u.msg)
|
close(u.done)
|
||||||
|
close(u.msg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume message buffer into an io.Writer. Will block, should be called in a
|
// Consume message buffer into an io.Writer. Will block, should be called in a
|
||||||
@ -85,16 +89,16 @@ func (u *User) Close() {
|
|||||||
// TODO: Not sure if this is a great API.
|
// TODO: Not sure if this is a great API.
|
||||||
func (u *User) Consume(out io.Writer) {
|
func (u *User) Consume(out io.Writer) {
|
||||||
for m := range u.msg {
|
for m := range u.msg {
|
||||||
u.handleMsg(m, out)
|
u.HandleMsg(m, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume one message and stop, mostly for testing
|
// Consume one message and stop, mostly for testing
|
||||||
func (u *User) ConsumeOne(out io.Writer) {
|
func (u *User) ConsumeOne(out io.Writer) {
|
||||||
u.handleMsg(<-u.msg, out)
|
u.HandleMsg(<-u.msg, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) handleMsg(m Message, out io.Writer) {
|
func (u *User) HandleMsg(m Message, out io.Writer) {
|
||||||
s := m.Render(u.Config.Theme)
|
s := m.Render(u.Config.Theme)
|
||||||
_, err := out.Write([]byte(s + Newline))
|
_, err := out.Write([]byte(s + Newline))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
21
host.go
21
host.go
@ -29,14 +29,22 @@ func NewHost(listener *sshd.SSHListener) *Host {
|
|||||||
|
|
||||||
// Connect a specific Terminal to this host and its channel
|
// Connect a specific Terminal to this host and its channel
|
||||||
func (h *Host) Connect(term *sshd.Terminal) {
|
func (h *Host) Connect(term *sshd.Terminal) {
|
||||||
defer term.Close()
|
|
||||||
name := term.Conn.User()
|
name := term.Conn.User()
|
||||||
term.SetPrompt(fmt.Sprintf("[%s] ", name))
|
|
||||||
term.AutoCompleteCallback = h.AutoCompleteFunction
|
term.AutoCompleteCallback = h.AutoCompleteFunction
|
||||||
|
|
||||||
user := chat.NewUserScreen(name, term)
|
user := chat.NewUserScreen(name, term)
|
||||||
|
go func() {
|
||||||
|
// Close term once user is closed.
|
||||||
|
user.Wait()
|
||||||
|
term.Close()
|
||||||
|
}()
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|
||||||
|
refreshPrompt := func() {
|
||||||
|
term.SetPrompt(fmt.Sprintf("[%s] ", user.Name()))
|
||||||
|
}
|
||||||
|
refreshPrompt()
|
||||||
|
|
||||||
err := h.channel.Join(user)
|
err := h.channel.Join(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to join: %s", err)
|
logger.Errorf("Failed to join: %s", err)
|
||||||
@ -54,7 +62,14 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
m := chat.ParseInput(line, user)
|
m := chat.ParseInput(line, user)
|
||||||
h.channel.Send(m)
|
// FIXME: Any reason to use h.channel.Send(m) instead?
|
||||||
|
h.channel.HandleMsg(m)
|
||||||
|
if m.Command() == "/nick" {
|
||||||
|
// Hijack /nick command to update terminal synchronously. Wouldn't
|
||||||
|
// work if we use h.channel.Send(m) above.
|
||||||
|
// FIXME: This is hacky, how do we improve the API to allow for this?
|
||||||
|
refreshPrompt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.channel.Leave(user)
|
err = h.channel.Leave(user)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user