/kick and /msg

This commit is contained in:
Andrey Petrov 2015-01-10 18:05:31 -08:00
parent 11e92b5718
commit e626eab624
7 changed files with 122 additions and 57 deletions

View File

@ -142,12 +142,20 @@ func (ch *Channel) Leave(u *User) error {
// Member returns a corresponding Member object to a User if the Member is // Member returns a corresponding Member object to a User if the Member is
// present in this channel. // present in this channel.
func (ch *Channel) Member(u *User) (*Member, bool) { func (ch *Channel) Member(u *User) (*Member, bool) {
m, err := ch.members.Get(u.Id()) m, ok := ch.MemberById(u.Id())
if err != nil { if !ok {
return nil, false return nil, false
} }
// Check that it's the same user // 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 nil, false
} }
return m.(*Member), true return m.(*Member), true

View File

@ -86,7 +86,7 @@ func (c Commands) Help(showOp bool) string {
} }
help := "Available commands:" + Newline + NewCommandsHelp(normal).String() help := "Available commands:" + Newline + NewCommandsHelp(normal).String()
if showOp { if showOp {
help += Newline + "Operator commands:" + Newline + NewCommandsHelp(op).String() help += Newline + "-> Operator commands:" + Newline + NewCommandsHelp(op).String()
} }
return help return help
} }
@ -231,12 +231,12 @@ func InitCommands(c *Commands) {
// TODO: Add support for fingerprint-based op'ing. This will // TODO: Add support for fingerprint-based op'ing. This will
// probably need to live in host land. // probably need to live in host land.
member, err := channel.members.Get(Id(args[0])) member, ok := channel.MemberById(Id(args[0]))
if err != nil { if !ok {
return errors.New("user not found") return errors.New("user not found")
} }
member.(*Member).Op = true member.Op = true
return nil return nil
}, },
}) })

View File

@ -20,6 +20,7 @@ type User struct {
joined time.Time joined time.Time
msg chan Message msg chan Message
done chan struct{} done chan struct{}
replyTo *User // Set when user gets a /msg, for replying.
closed bool closed bool
closeOnce sync.Once closeOnce sync.Once
} }

39
command.go Normal file
View File

@ -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
},
})
}

72
host.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"strings" "strings"
@ -9,6 +10,15 @@ import (
"github.com/shazow/ssh-chat/sshd" "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 // Host is the bridge between sshd and chat modules
// TODO: Should be easy to add support for multiple channels, if we want. // TODO: Should be easy to add support for multiple channels, if we want.
type Host struct { type Host struct {
@ -35,6 +45,7 @@ func NewHost(listener *sshd.SSHListener) *Host {
// Make our own commands registry instance. // Make our own commands registry instance.
commands := chat.Commands{} commands := chat.Commands{}
chat.InitCommands(&commands) chat.InitCommands(&commands)
h.InitCommands(&commands)
ch.SetCommands(commands) ch.SetCommands(commands)
go ch.Serve() go ch.Serve()
@ -159,11 +170,58 @@ func (h *Host) AutoCompleteFunction(line string, pos int, key rune) (newLine str
return return
} }
// GetPrompt will render the terminal prompt string based on the user. // InitCommands adds host-specific commands to a Commands container. These will
func GetPrompt(user *chat.User) string { // override any existing commands.
name := user.Name() func (h *Host) InitCommands(c *chat.Commands) {
if user.Config.Theme != nil { c.Add(chat.Command{
name = user.Config.Theme.ColorName(user) Prefix: "/msg",
} PrefixHelp: "USER MESSAGE",
return fmt.Sprintf("[%s] ", name) 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
},
})
} }

View File

@ -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
}

View File

@ -1,8 +1,9 @@
// Borrowed from go.crypto circa 2011
package sshd package sshd
import "encoding/binary" import "encoding/binary"
// Helpers below are borrowed from go.crypto circa 2011:
// parsePtyRequest parses the payload of the pty-req message and extracts the // parsePtyRequest parses the payload of the pty-req message and extracts the
// dimensions of the terminal. See RFC 4254, section 6.2. // dimensions of the terminal. See RFC 4254, section 6.2.
func parsePtyRequest(s []byte) (width, height int, ok bool) { func parsePtyRequest(s []byte) (width, height int, ok bool) {