progress: Host User interface, and more interfaces in general, tests pass

This commit is contained in:
Andrey Petrov 2016-09-15 14:01:15 -04:00
parent d16b1f5829
commit e86996e6b5
6 changed files with 72 additions and 90 deletions

View File

@ -1,6 +1,8 @@
package chat package chat
import ( import (
"time"
"github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/set" "github.com/shazow/ssh-chat/set"
) )
@ -21,4 +23,10 @@ type Member interface {
SetConfig(message.UserConfig) SetConfig(message.UserConfig)
Send(message.Message) error Send(message.Message) error
Joined() time.Time
ReplyTo() message.Author
SetReplyTo(message.Author)
Prompt() string
SetHighlight(string) error
} }

View File

@ -38,3 +38,7 @@ func (u *User) Color() int {
func (u *User) ID() string { func (u *User) ID() string {
return SanitizeName(u.name) return SanitizeName(u.name)
} }
func (u *User) Joined() time.Time {
return u.joined
}

View File

@ -199,7 +199,8 @@ func (r *Room) MemberByID(id string) (*roomMember, bool) {
if err != nil { if err != nil {
return nil, false return nil, false
} }
return m.Value().(*roomMember), true rm, ok := m.Value().(*roomMember)
return rm, ok
} }
// IsOp returns whether a user is an operator in this room. // IsOp returns whether a user is an operator in this room.

View File

@ -1,21 +1,33 @@
package sshchat package sshchat
import ( import (
"time" "sync"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
type Client struct { type client struct {
user chat.Member chat.Member
conn sshd.Connection sync.Mutex
conns []sshd.Connection
timestamp time.Time
} }
type Replier interface { func (cl *client) Connections() []sshd.Connection {
ReplyTo() message.Author return cl.conns
SetReplyTo(message.Author) }
func (cl *client) Close() error {
// TODO: Stack errors?
for _, conn := range cl.conns {
conn.Close()
}
return nil
}
type User interface {
chat.Member
Connections() []sshd.Connection
Close() error
} }

92
host.go
View File

@ -35,7 +35,7 @@ type Host struct {
mu sync.Mutex mu sync.Mutex
motd string motd string
count int count int
clients map[chat.Member][]Client clients map[chat.Member][]client
} }
// NewHost creates a Host on top of an existing listener. // NewHost creates a Host on top of an existing listener.
@ -46,7 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host {
listener: listener, listener: listener,
commands: chat.Commands{}, commands: chat.Commands{},
auth: auth, auth: auth,
clients: map[chat.Member][]Client{}, clients: map[chat.Member][]client{},
} }
// Make our own commands registry instance. // Make our own commands registry instance.
@ -75,10 +75,11 @@ func (h *Host) SetMotd(motd string) {
// Connect a specific Terminal to this host and its room. // Connect a specific Terminal to this host and its room.
func (h *Host) Connect(term *sshd.Terminal) { func (h *Host) Connect(term *sshd.Terminal) {
requestedName := term.Conn.Name() requestedName := term.Conn.Name()
user := message.BufferedScreen(requestedName, term) screen := message.BufferedScreen(requestedName, term)
user := &client{
client := h.addClient(user, term.Conn) Member: screen,
defer h.removeClient(user, client) conns: []sshd.Connection{term.Conn},
}
h.mu.Lock() h.mu.Lock()
motd := h.motd motd := h.motd
@ -91,10 +92,10 @@ func (h *Host) Connect(term *sshd.Terminal) {
user.SetConfig(cfg) user.SetConfig(cfg)
// Close term once user is closed. // Close term once user is closed.
defer user.Close() defer screen.Close()
defer term.Close() defer term.Close()
go user.Consume() go screen.Consume()
// Send MOTD // Send MOTD
if motd != "" { if motd != "" {
@ -180,44 +181,6 @@ func (h *Host) Connect(term *sshd.Terminal) {
logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name())
} }
func (h *Host) addClient(user chat.Member, conn sshd.Connection) *Client {
client := Client{
user: user,
conn: conn,
timestamp: time.Now(),
}
h.mu.Lock()
if _, ok := h.clients[user]; ok {
logger.Warningf("user collision: %q", user)
}
h.clients[user] = append(h.clients[user], client)
h.mu.Unlock()
return &client
}
func (h *Host) removeClient(user chat.Member, client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
clients := h.clients[user]
for i, c := range clients {
// Find the user
if &c != client {
continue
}
// Delete corresponding client
clients[i] = clients[len(clients)-1]
clients = clients[:len(clients)-1]
break
}
}
func (h *Host) findClients(user chat.Member) []Client {
h.mu.Lock()
defer h.mu.Unlock()
return h.clients[user]
}
// Serve our chat room onto the listener // Serve our chat room onto the listener
func (h *Host) Serve() { func (h *Host) Serve() {
h.listener.HandlerFunc = h.Connect h.listener.HandlerFunc = h.Connect
@ -244,7 +207,7 @@ func (h *Host) completeCommand(partial string) string {
} }
// AutoCompleteFunction returns a callback for terminal autocompletion // AutoCompleteFunction returns a callback for terminal autocompletion
func (h *Host) AutoCompleteFunction(u Replier) func(line string, pos int, key rune) (newLine string, newPos int, ok bool) { func (h *Host) AutoCompleteFunction(u 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) { return func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
if key != 9 { if key != 9 {
return return
@ -301,12 +264,13 @@ func (h *Host) AutoCompleteFunction(u Replier) func(line string, pos int, key ru
} }
// GetUser returns a message.User based on a name. // GetUser returns a message.User based on a name.
func (h *Host) GetUser(name string) (chat.Member, bool) { func (h *Host) GetUser(name string) (User, bool) {
m, ok := h.MemberByID(name) m, ok := h.MemberByID(name)
if !ok { if !ok {
return nil, false return nil, false
} }
return m.Member, true u, ok := m.Member.(User)
return u, ok
} }
// InitCommands adds host-specific commands to a Commands container. These will // InitCommands adds host-specific commands to a Commands container. These will
@ -336,7 +300,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
txt := fmt.Sprintf("[Sent PM to %s]", target.Name()) txt := fmt.Sprintf("[Sent PM to %s]", target.Name())
ms := message.NewSystemMsg(txt, msg.From()) ms := message.NewSystemMsg(txt, msg.From())
room.Send(ms) room.Send(ms)
target.(Replier).SetReplyTo(msg.From()) target.SetReplyTo(msg.From())
return nil return nil
}, },
}) })
@ -352,7 +316,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("must specify message") return errors.New("must specify message")
} }
target := msg.From().(Replier).ReplyTo() target := msg.From().(chat.Member).ReplyTo()
if target == nil { if target == nil {
return errors.New("no message to reply to") return errors.New("no message to reply to")
} }
@ -388,14 +352,12 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("user not found") return errors.New("user not found")
} }
// FIXME: Handle many clients
clients := h.findClients(target)
var whois string var whois string
switch room.IsOp(msg.From().(chat.Member)) { switch room.IsOp(msg.From().(chat.Member)) {
case true: case true:
whois = whoisAdmin(clients) whois = whoisAdmin(target)
case false: case false:
whois = whoisPublic(clients) whois = whoisPublic(target)
} }
room.Send(message.NewSystemMsg(whois, msg.From())) room.Send(message.NewSystemMsg(whois, msg.From()))
@ -451,9 +413,12 @@ func (h *Host) InitCommands(c *chat.Commands) {
room.Ops.Add(set.Keyize(user.ID())) room.Ops.Add(set.Keyize(user.ID()))
} }
for _, client := range h.findClients(user) { // TODO: Add pubkeys to op
h.auth.Op(client.conn.PublicKey(), until) /*
} for _, conn := range user.Connections() {
h.auth.Op(conn.PublicKey(), until)
}
*/
body := fmt.Sprintf("Made op by %s.", msg.From().Name()) body := fmt.Sprintf("Made op by %s.", msg.From().Name())
room.Send(message.NewSystemMsg(body, user)) room.Send(message.NewSystemMsg(body, user))
@ -489,17 +454,16 @@ func (h *Host) InitCommands(c *chat.Commands) {
until, _ = time.ParseDuration(args[1]) until, _ = time.ParseDuration(args[1])
} }
clients := h.findClients(target) for _, conn := range target.Connections() {
for _, client := range clients { h.auth.Ban(conn.PublicKey(), until)
h.auth.Ban(client.conn.PublicKey(), until) h.auth.BanAddr(conn.RemoteAddr(), until)
h.auth.BanAddr(client.conn.RemoteAddr(), until)
} }
body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
room.Send(message.NewAnnounceMsg(body)) room.Send(message.NewAnnounceMsg(body))
target.(io.Closer).Close() target.Close()
logger.Debugf("Banned: \n-> %s", whoisAdmin(clients)) logger.Debugf("Banned: \n-> %s", whoisAdmin(target))
return nil return nil
}, },

View File

@ -2,7 +2,6 @@ package sshchat
import ( import (
"net" "net"
"time"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/chat/message"
@ -11,28 +10,22 @@ import (
// Helpers for printing whois messages // Helpers for printing whois messages
type joinTimestamped interface { func whoisPublic(u User) string {
Joined() time.Time
}
func whoisPublic(clients []Client) string {
// FIXME: Handle many clients
conn, u := clients[0].conn, clients[0].user
fingerprint := "(no public key)" fingerprint := "(no public key)"
// FIXME: Use all connections?
conn := u.Connections()[0]
if conn.PublicKey() != nil { if conn.PublicKey() != nil {
fingerprint = sshd.Fingerprint(conn.PublicKey()) fingerprint = sshd.Fingerprint(conn.PublicKey())
} }
return "name: " + u.Name() + message.Newline + return "name: " + u.Name() + message.Newline +
" > fingerprint: " + fingerprint + message.Newline + " > fingerprint: " + fingerprint + message.Newline +
" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
" > joined: " + humanize.Time(u.(joinTimestamped).Joined()) " > joined: " + humanize.Time(u.Joined())
} }
func whoisAdmin(clients []Client) string { func whoisAdmin(u User) string {
// FIXME: Handle many clients // FIXME: Use all connections?
conn, u := clients[0].conn, clients[0].user conn := u.Connections()[0]
ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
fingerprint := "(no public key)" fingerprint := "(no public key)"
if conn.PublicKey() != nil { if conn.PublicKey() != nil {
@ -42,5 +35,5 @@ func whoisAdmin(clients []Client) string {
" > ip: " + ip + message.Newline + " > ip: " + ip + message.Newline +
" > fingerprint: " + fingerprint + message.Newline + " > fingerprint: " + fingerprint + message.Newline +
" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
" > joined: " + humanize.Time(u.(joinTimestamped).Joined()) " > joined: " + humanize.Time(u.Joined())
} }