diff --git a/chat/command.go b/chat/command.go index 536de69..63423c0 100644 --- a/chat/command.go +++ b/chat/command.go @@ -477,4 +477,38 @@ func InitCommands(c *Commands) { return nil }, }) + + c.Add(Command{ + Op: true, + Prefix: "/mute", + PrefixHelp: "USER", + Help: "Toggle muting USER, preventing messages from broadcasting.", + Handler: func(room *Room, msg message.CommandMsg) error { + if !room.IsOp(msg.From()) { + return errors.New("must be op") + } + + args := msg.Args() + if len(args) == 0 { + return errors.New("must specify user") + } + + member, ok := room.MemberByID(args[0]) + if !ok { + return errors.New("user not found") + } + + setMute := !member.IsMuted() + member.SetMute(setMute) + id := member.ID() + + if setMute { + room.Send(message.NewSystemMsg("Muted: "+id, msg.From())) + } else { + room.Send(message.NewSystemMsg("Unmuted: "+id, msg.From())) + } + + return nil + }, + }) } diff --git a/chat/logger.go b/chat/logger.go index 477e1d3..0f4087d 100644 --- a/chat/logger.go +++ b/chat/logger.go @@ -1,7 +1,9 @@ package chat -import "io" -import stdlog "log" +import ( + "io" + stdlog "log" +) var logger *stdlog.Logger diff --git a/chat/room.go b/chat/room.go index b126845..9539ec9 100644 --- a/chat/room.go +++ b/chat/room.go @@ -27,6 +27,23 @@ var ErrInvalidName = errors.New("invalid name") type Member struct { *message.User IsOp bool + + // TODO: Move IsOp under mu? + + mu sync.Mutex + isMuted bool // When true, messages should not be broadcasted. +} + +func (m *Member) IsMuted() bool { + m.mu.Lock() + defer m.mu.Unlock() + return m.isMuted +} + +func (m *Member) SetMute(muted bool) { + m.mu.Lock() + defer m.mu.Unlock() + m.isMuted = muted } // Room definition, also a Set of User Items @@ -84,6 +101,20 @@ func (r *Room) HandleMsg(m message.Message) { fromID = fromMsg.From().ID() } + if fromID != "" { + if item, err := r.Members.Get(fromID); err != nil { + // Message from a member who is not in the room, this should not happen. + logger.Printf("Room received unexpected message from a non-member: %v", m) + return + } else if member, ok := item.Value().(*Member); ok && member.IsMuted() { + // Short circuit message handling for muted users + if _, ok = m.(*message.CommandMsg); !ok { + member.User.Send(m) + } + return + } + } + switch m := m.(type) { case *message.CommandMsg: cmd := *m @@ -150,6 +181,7 @@ func (r *Room) Join(u *message.User) (*Member, error) { if err != nil { return nil, err } + // TODO: Remove user ID from sets, probably referring to a prior user. r.History(u) s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.Members.Len()) r.Send(message.NewAnnounceMsg(s)) diff --git a/chat/room_test.go b/chat/room_test.go index 44c8f0c..2f78961 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -158,6 +158,94 @@ func TestIgnore(t *testing.T) { expectOutput(t, buffer, ignored.user.Name()+": hello again!"+message.Newline) } +func TestMute(t *testing.T) { + var buffer []byte + + ch := NewRoom() + go ch.Serve() + defer ch.Close() + + // Create 3 users, join the room and clear their screen buffers + users := make([]ScreenedUser, 3) + members := make([]*Member, 3) + for i := 0; i < 3; i++ { + screen := &MockScreen{} + user := message.NewUserScreen(message.SimpleID(fmt.Sprintf("user%d", i)), screen) + users[i] = ScreenedUser{ + user: user, + screen: screen, + } + + member, err := ch.Join(user) + if err != nil { + t.Fatal(err) + } + members[i] = member + } + + for _, u := range users { + for i := 0; i < 3; i++ { + u.user.HandleMsg(u.user.ConsumeOne()) + u.screen.Read(&buffer) + } + } + + // Use some handy variable names for distinguish between roles + muter := users[0] + muted := users[1] + other := users[2] + + members[0].IsOp = true + + // test muting unexisting user + if err := sendCommand("/mute test", muter, ch, &buffer); err != nil { + t.Fatal(err) + } + expectOutput(t, buffer, "-> Err: user not found"+message.Newline) + + // test muting by non-op + if err := sendCommand("/mute "+muted.user.Name(), other, ch, &buffer); err != nil { + t.Fatal(err) + } + expectOutput(t, buffer, "-> Err: must be op"+message.Newline) + + // test muting existing user + if err := sendCommand("/mute "+muted.user.Name(), muter, ch, &buffer); err != nil { + t.Fatal(err) + } + expectOutput(t, buffer, "-> Muted: "+muted.user.Name()+message.Newline) + + if got, want := members[1].IsMuted(), true; got != want { + t.Error("muted user failed to set mute flag") + } + + // when an emote is sent by a muted user, it should not be displayed for anyone + ch.HandleMsg(message.NewPublicMsg("hello!", muted.user)) + ch.HandleMsg(message.NewEmoteMsg("is crying", muted.user)) + + if muter.user.HasMessages() { + muter.user.HandleMsg(muter.user.ConsumeOne()) + muter.screen.Read(&buffer) + t.Errorf("muter should not have messages: %s", buffer) + } + if other.user.HasMessages() { + other.user.HandleMsg(other.user.ConsumeOne()) + other.screen.Read(&buffer) + t.Errorf("other should not have messages: %s", buffer) + } + + // test unmuting + if err := sendCommand("/mute "+muted.user.Name(), muter, ch, &buffer); err != nil { + t.Fatal(err) + } + expectOutput(t, buffer, "-> Unmuted: "+muted.user.Name()+message.Newline) + + ch.HandleMsg(message.NewPublicMsg("hello again!", muted.user)) + other.user.HandleMsg(other.user.ConsumeOne()) + other.screen.Read(&buffer) + expectOutput(t, buffer, muted.user.Name()+": hello again!"+message.Newline) +} + func expectOutput(t *testing.T, buffer []byte, expected string) { t.Helper()