diff --git a/chat/room.go b/chat/room.go index 31dc393..71e3171 100644 --- a/chat/room.go +++ b/chat/room.go @@ -25,6 +25,7 @@ var ErrInvalidName = errors.New("invalid name") // Member is a User with per-Room metadata attached to it. type Member struct { *message.User + IsOp bool } // Room definition, also a Set of User Items @@ -37,7 +38,6 @@ type Room struct { closeOnce sync.Once Members *set.Set - Ops *set.Set } // NewRoom creates a new room. @@ -50,7 +50,6 @@ func NewRoom() *Room { commands: *defaultCommands, Members: set.New(), - Ops: set.New(), } } @@ -148,7 +147,7 @@ func (r *Room) Join(u *message.User) (*Member, error) { if u.ID() == "" { return nil, ErrInvalidName } - member := &Member{u} + member := &Member{User: u} err := r.Members.Add(set.Itemize(u.ID(), member)) if err != nil { return nil, err @@ -165,7 +164,6 @@ func (r *Room) Leave(u *message.User) error { if err != nil { return err } - r.Ops.Remove(u.ID()) s := fmt.Sprintf("%s left. (Connected %s)", u.Name(), humantime.Since(u.Joined())) r.Send(message.NewAnnounceMsg(s)) 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. 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. diff --git a/host.go b/host.go index cd30f40..a7a379c 100644 --- a/host.go +++ b/host.go @@ -13,7 +13,6 @@ import ( "github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/internal/humantime" - "github.com/shazow/ssh-chat/set" "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? if h.isOp(term.Conn) { - h.Room.Ops.Add(set.Itemize(member.ID(), member)) + member.IsOp = true } ratelimit := rateio.NewSimpleLimiter(3, time.Second*3) @@ -523,8 +522,8 @@ func (h *Host) InitCommands(c *chat.Commands) { c.Add(chat.Command{ Op: true, Prefix: "/op", - PrefixHelp: "USER [DURATION]", - Help: "Set USER as admin.", + PrefixHelp: "USER [DURATION|remove]", + Help: "Set USER as admin. Duration only applies to pubkey reconnects.", Handler: func(room *chat.Room, msg message.CommandMsg) error { if !room.IsOp(msg.From()) { return errors.New("must be op") @@ -535,21 +534,33 @@ func (h *Host) InitCommands(c *chat.Commands) { return errors.New("must specify user") } + opValue := true var until time.Duration 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]) if !ok { return errors.New("user not found") } - room.Ops.Add(set.Itemize(member.ID(), member)) + member.IsOp = opValue id := member.Identifier.(*Identity) 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)) return nil diff --git a/host_test.go b/host_test.go index f98724a..b4ee362 100644 --- a/host_test.go +++ b/host_test.go @@ -6,12 +6,10 @@ import ( "crypto/rsa" "errors" "io" - "io/ioutil" "strings" "testing" "github.com/shazow/ssh-chat/chat/message" - "github.com/shazow/ssh-chat/set" "github.com/shazow/ssh-chat/sshd" "golang.org/x/crypto/ssh" ) @@ -186,21 +184,43 @@ func TestHostKick(t *testing.T) { go host.Serve() connected := make(chan struct{}) + kicked := make(chan struct{}) done := make(chan struct{}) go func() { // First client 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 member, _ := host.Room.MemberByID("foo") if member == nil { 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 connected <- struct{}{} + scanner.Scan() // Connected message + 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 }) if err != nil { @@ -213,11 +233,14 @@ func TestHostKick(t *testing.T) { go func() { // Second client err := sshd.ConnectShell(addr, "bar", func(r io.Reader, w io.WriteCloser) error { + scanner := bufio.NewScanner(r) <-connected + scanner.Scan() - // Consume while we're connected. Should break when kicked. - ioutil.ReadAll(r) - return nil + <-kicked + + scanner.Scan() + return scanner.Err() }) if err != nil { close(done)