/op: Fix room op being tied to the nick, add remove option

Closes #301
This commit is contained in:
Andrey Petrov 2019-03-15 18:18:20 -04:00
parent 953c3d46b2
commit 60701198fc
3 changed files with 54 additions and 18 deletions

View File

@ -25,6 +25,7 @@ var ErrInvalidName = errors.New("invalid name")
// Member is a User with per-Room metadata attached to it. // Member is a User with per-Room metadata attached to it.
type Member struct { type Member struct {
*message.User *message.User
IsOp bool
} }
// Room definition, also a Set of User Items // Room definition, also a Set of User Items
@ -37,7 +38,6 @@ type Room struct {
closeOnce sync.Once closeOnce sync.Once
Members *set.Set Members *set.Set
Ops *set.Set
} }
// NewRoom creates a new room. // NewRoom creates a new room.
@ -50,7 +50,6 @@ func NewRoom() *Room {
commands: *defaultCommands, commands: *defaultCommands,
Members: set.New(), Members: set.New(),
Ops: set.New(),
} }
} }
@ -148,7 +147,7 @@ func (r *Room) Join(u *message.User) (*Member, error) {
if u.ID() == "" { if u.ID() == "" {
return nil, ErrInvalidName return nil, ErrInvalidName
} }
member := &Member{u} member := &Member{User: u}
err := r.Members.Add(set.Itemize(u.ID(), member)) err := r.Members.Add(set.Itemize(u.ID(), member))
if err != nil { if err != nil {
return nil, err return nil, err
@ -165,7 +164,6 @@ func (r *Room) Leave(u *message.User) error {
if err != nil { if err != nil {
return err return err
} }
r.Ops.Remove(u.ID())
s := fmt.Sprintf("%s left. (Connected %s)", u.Name(), humantime.Since(u.Joined())) s := fmt.Sprintf("%s left. (Connected %s)", u.Name(), humantime.Since(u.Joined()))
r.Send(message.NewAnnounceMsg(s)) r.Send(message.NewAnnounceMsg(s))
return nil return nil
@ -211,7 +209,11 @@ func (r *Room) MemberByID(id string) (*Member, bool) {
// IsOp returns whether a user is an operator in this room. // IsOp returns whether a user is an operator in this room.
func (r *Room) IsOp(u *message.User) bool { func (r *Room) IsOp(u *message.User) bool {
return r.Ops.In(u.ID()) m, ok := r.Member(u)
if !ok {
return false
}
return m.IsOp
} }
// Topic of the room. // Topic of the room.

25
host.go
View File

@ -13,7 +13,6 @@ import (
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/internal/humantime" "github.com/shazow/ssh-chat/internal/humantime"
"github.com/shazow/ssh-chat/set"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
@ -131,7 +130,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
// Should the user be op'd on join? // Should the user be op'd on join?
if h.isOp(term.Conn) { if h.isOp(term.Conn) {
h.Room.Ops.Add(set.Itemize(member.ID(), member)) member.IsOp = true
} }
ratelimit := rateio.NewSimpleLimiter(3, time.Second*3) ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
@ -523,8 +522,8 @@ func (h *Host) InitCommands(c *chat.Commands) {
c.Add(chat.Command{ c.Add(chat.Command{
Op: true, Op: true,
Prefix: "/op", Prefix: "/op",
PrefixHelp: "USER [DURATION]", PrefixHelp: "USER [DURATION|remove]",
Help: "Set USER as admin.", Help: "Set USER as admin. Duration only applies to pubkey reconnects.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
if !room.IsOp(msg.From()) { if !room.IsOp(msg.From()) {
return errors.New("must be op") return errors.New("must be op")
@ -535,21 +534,33 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("must specify user") return errors.New("must specify user")
} }
opValue := true
var until time.Duration var until time.Duration
if len(args) > 1 { if len(args) > 1 {
until, _ = time.ParseDuration(args[1]) if args[1] == "remove" {
// Expire instantly
until = time.Duration(1)
opValue = false
} else {
until, _ = time.ParseDuration(args[1])
}
} }
member, ok := room.MemberByID(args[0]) member, ok := room.MemberByID(args[0])
if !ok { if !ok {
return errors.New("user not found") return errors.New("user not found")
} }
room.Ops.Add(set.Itemize(member.ID(), member)) member.IsOp = opValue
id := member.Identifier.(*Identity) id := member.Identifier.(*Identity)
h.auth.Op(id.PublicKey(), until) h.auth.Op(id.PublicKey(), until)
body := fmt.Sprintf("Made op by %s.", msg.From().Name()) var body string
if opValue {
body = fmt.Sprintf("Made op by %s.", msg.From().Name())
} else {
body = fmt.Sprintf("Removed op by %s.", msg.From().Name())
}
room.Send(message.NewSystemMsg(body, member.User)) room.Send(message.NewSystemMsg(body, member.User))
return nil return nil

View File

@ -6,12 +6,10 @@ import (
"crypto/rsa" "crypto/rsa"
"errors" "errors"
"io" "io"
"io/ioutil"
"strings" "strings"
"testing" "testing"
"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/sshd" "github.com/shazow/ssh-chat/sshd"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@ -186,21 +184,43 @@ func TestHostKick(t *testing.T) {
go host.Serve() go host.Serve()
connected := make(chan struct{}) connected := make(chan struct{})
kicked := make(chan struct{})
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
// First client // First client
err := sshd.ConnectShell(addr, "foo", func(r io.Reader, w io.WriteCloser) error { err := sshd.ConnectShell(addr, "foo", func(r io.Reader, w io.WriteCloser) error {
scanner := bufio.NewScanner(r)
// Consume the initial buffer
scanner.Scan()
// Make op // Make op
member, _ := host.Room.MemberByID("foo") member, _ := host.Room.MemberByID("foo")
if member == nil { if member == nil {
return errors.New("failed to load MemberByID") return errors.New("failed to load MemberByID")
} }
host.Room.Ops.Add(set.Itemize(member.ID(), member)) member.IsOp = true
// Change nicks, make sure op sticks
w.Write([]byte("/nick quux\r\n"))
scanner.Scan() // Prompt
scanner.Scan() // Nick change response
// Block until second client is here // Block until second client is here
connected <- struct{}{} connected <- struct{}{}
scanner.Scan() // Connected message
w.Write([]byte("/kick bar\r\n")) w.Write([]byte("/kick bar\r\n"))
scanner.Scan() // Prompt
scanner.Scan()
if actual, expected := stripPrompt(scanner.Text()), " * bar was kicked by quux.\r"; actual != expected {
t.Errorf("Got %q; expected %q", actual, expected)
}
kicked <- struct{}{}
return nil return nil
}) })
if err != nil { if err != nil {
@ -213,11 +233,14 @@ func TestHostKick(t *testing.T) {
go func() { go func() {
// Second client // Second client
err := sshd.ConnectShell(addr, "bar", func(r io.Reader, w io.WriteCloser) error { err := sshd.ConnectShell(addr, "bar", func(r io.Reader, w io.WriteCloser) error {
scanner := bufio.NewScanner(r)
<-connected <-connected
scanner.Scan()
// Consume while we're connected. Should break when kicked. <-kicked
ioutil.ReadAll(r)
return nil scanner.Scan()
return scanner.Err()
}) })
if err != nil { if err != nil {
close(done) close(done)