From 5f201e0f203bbb91596be7e1379bbe094353acab Mon Sep 17 00:00:00 2001 From: Pavel Zaitsev Date: Tue, 7 Jul 2020 23:24:25 -0400 Subject: [PATCH 01/12] now if both are ops it will be reflected in output of whois command --- host.go | 3 +-- identity.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/host.go b/host.go index 8973229..16595c3 100644 --- a/host.go +++ b/host.go @@ -407,12 +407,11 @@ func (h *Host) InitCommands(c *chat.Commands) { if !ok { return errors.New("user not found") } - id := target.Identifier.(*Identity) var whois string switch room.IsOp(msg.From()) { case true: - whois = id.WhoisAdmin() + whois = id.WhoisAdmin(room, h) case false: whois = id.Whois() } diff --git a/identity.go b/identity.go index 0817792..ed74d62 100644 --- a/identity.go +++ b/identity.go @@ -4,6 +4,7 @@ import ( "net" "time" + "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/internal/sanitize" @@ -59,13 +60,21 @@ func (i Identity) Whois() string { } // WhoisAdmin returns a whois description for admin users. -func (i Identity) WhoisAdmin() string { +func (i Identity) WhoisAdmin(room *chat.Room, host *Host) string { ip, _, _ := net.SplitHostPort(i.RemoteAddr().String()) fingerprint := "(no public key)" if i.PublicKey() != nil { fingerprint = sshd.Fingerprint(i.PublicKey()) } + + isOp := "" + user, ok := host.GetUser(i.id) + if ok && room.IsOp(user) { + isOp = " > Op" + message.Newline + } + return "name: " + i.Name() + message.Newline + + isOp + " > ip: " + ip + message.Newline + " > fingerprint: " + fingerprint + message.Newline + " > client: " + sanitize.Data(string(i.ClientVersion()), 64) + message.Newline + From 09c8bbdd4c9d02050d79b3aca7ffb318b4264377 Mon Sep 17 00:00:00 2001 From: Lucas Hourahine Date: Thu, 16 Jul 2020 13:25:14 -0400 Subject: [PATCH 02/12] sorting nicks on /names and /list --- chat/command.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chat/command.go b/chat/command.go index 182d00f..fe9355e 100644 --- a/chat/command.go +++ b/chat/command.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" "time" + "sort" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/internal/sanitize" @@ -189,6 +190,7 @@ func InitCommands(c *Commands) { } names := room.Members.ListPrefix("") + sort.Slice(names, func(i, j int) bool { return names[i].Key() < names[j].Key() }) colNames := make([]string, len(names)) for i, uname := range names { colNames[i] = colorize(uname.Value().(*Member).User) From b6a8763f3be65e436c394e58ef9e1cbbc2954179 Mon Sep 17 00:00:00 2001 From: Pavel Zaitsev Date: Wed, 15 Jul 2020 00:32:53 -0400 Subject: [PATCH 03/12] updated in line with comments in PR * reduce change footprint to parameter list * moved Op flag display to last line as to not break bots --- host.go | 2 +- identity.go | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/host.go b/host.go index 16595c3..1c615c0 100644 --- a/host.go +++ b/host.go @@ -411,7 +411,7 @@ func (h *Host) InitCommands(c *chat.Commands) { var whois string switch room.IsOp(msg.From()) { case true: - whois = id.WhoisAdmin(room, h) + whois = id.WhoisAdmin(room) case false: whois = id.Whois() } diff --git a/identity.go b/identity.go index ed74d62..93df4e5 100644 --- a/identity.go +++ b/identity.go @@ -60,7 +60,7 @@ func (i Identity) Whois() string { } // WhoisAdmin returns a whois description for admin users. -func (i Identity) WhoisAdmin(room *chat.Room, host *Host) string { +func (i Identity) WhoisAdmin(room *chat.Room) string { ip, _, _ := net.SplitHostPort(i.RemoteAddr().String()) fingerprint := "(no public key)" if i.PublicKey() != nil { @@ -68,15 +68,14 @@ func (i Identity) WhoisAdmin(room *chat.Room, host *Host) string { } isOp := "" - user, ok := host.GetUser(i.id) - if ok && room.IsOp(user) { - isOp = " > Op" + message.Newline + if member, ok := room.MemberByID(i.ID()); ok && room.IsOp(member.User) { + isOp = message.Newline + " > op: true" } return "name: " + i.Name() + message.Newline + - isOp + " > ip: " + ip + message.Newline + " > fingerprint: " + fingerprint + message.Newline + " > client: " + sanitize.Data(string(i.ClientVersion()), 64) + message.Newline + - " > joined: " + humantime.Since(i.created) + " ago" + " > joined: " + humantime.Since(i.created) + " ago" + + isOp } From 5ff2ea085bba65aa3693ed3d2278cc494e8ddeff Mon Sep 17 00:00:00 2001 From: Pavel Zaitsev Date: Fri, 24 Jul 2020 10:16:29 -0400 Subject: [PATCH 04/12] in autocomplete list moves your name to last item in the list of sorted current users --- chat/room.go | 10 +++++++++- host.go | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chat/room.go b/chat/room.go index 5b34508..12f8ce8 100644 --- a/chat/room.go +++ b/chat/room.go @@ -226,7 +226,7 @@ func (r *Room) SetTopic(s string) { // NamesPrefix lists all members' names with a given prefix, used to query // for autocompletion purposes. Sorted by which user was last active. -func (r *Room) NamesPrefix(prefix string) []string { +func (r *Room) NamesPrefix(prefix string, current_user *message.User) []string { items := r.Members.ListPrefix(prefix) // Sort results by recently active @@ -235,6 +235,14 @@ func (r *Room) NamesPrefix(prefix string) []string { users = append(users, item.Value().(*Member).User) } sort.Sort(message.RecentActiveUsers(users)) + for i, user := range users { + if user.Name() == current_user.Name() { + // move it to the end. one user in the list? + save := users[0] + copy(users[i:], users[i+1:]) + users[len(users)-1] = save + } + } // Pull out names names := make([]string, 0, len(items)) diff --git a/host.go b/host.go index 1c615c0..808d9fc 100644 --- a/host.go +++ b/host.go @@ -243,8 +243,8 @@ func (h *Host) Serve() { h.listener.Serve() } -func (h *Host) completeName(partial string) string { - names := h.NamesPrefix(partial) +func (h *Host) completeName(partial string, current_user *message.User) string { + names := h.NamesPrefix(partial, current_user) if len(names) == 0 { // Didn't find anything return "" @@ -300,7 +300,7 @@ func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int, } } else { // Name - completed = h.completeName(partial) + completed = h.completeName(partial, u) if completed == "" { return } From e5374b7111e9005f146133ba5325eb515b3100fb Mon Sep 17 00:00:00 2001 From: Pavel Zaitsev Date: Fri, 24 Jul 2020 10:44:23 -0400 Subject: [PATCH 05/12] update, to fix tests. --- chat/room_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chat/room_test.go b/chat/room_test.go index c316e15..110c033 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -394,20 +394,20 @@ func TestRoomNamesPrefix(t *testing.T) { members[3].HandleMsg(message.NewMsg("hi")) // foo members[1].HandleMsg(message.NewMsg("hi")) // aab - if got, want := r.NamesPrefix("a"), []string{"aab", "aaa", "aac"}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("a", members[3].User), []string{"aab", "aaa", "aac"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } members[2].HandleMsg(message.NewMsg("hi")) // aac - if got, want := r.NamesPrefix("a"), []string{"aac", "aab", "aaa"}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("a", members[3].User), []string{"aac", "aab", "aaa"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } - if got, want := r.NamesPrefix("f"), []string{"foo"}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("f", members[0].User), []string{"foo"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } - if got, want := r.NamesPrefix("bar"), []string{}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("bar", members[3].User), []string{}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } } From 5885f7fbddd75f559c10c3b9bf74ba4fb50e981f Mon Sep 17 00:00:00 2001 From: Pavel Zaitsev Date: Mon, 27 Jul 2020 19:11:03 -0400 Subject: [PATCH 06/12] updated tests, moved code closer to the caller. * addded condition for zero time on lastMsg. * removed extra paramter in NamePrefix * moved code from NamePrefix to completeName * removed extra parameter in tests calling to NamePrefix --- chat/message/user.go | 8 +++++++- chat/room.go | 10 +--------- chat/room_test.go | 8 ++++---- host.go | 15 ++++++++++----- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/chat/message/user.go b/chat/message/user.go index 0fc8cc1..3f96e28 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -258,5 +258,11 @@ func (a RecentActiveUsers) Less(i, j int) bool { defer a[i].mu.Unlock() a[j].mu.Lock() defer a[j].mu.Unlock() - return a[i].lastMsg.After(a[j].lastMsg) + + if a[i].lastMsg.IsZero() { + return a[i].joined.Before(a[j].joined) + } else { + return a[i].lastMsg.After(a[j].lastMsg) + } + } diff --git a/chat/room.go b/chat/room.go index 12f8ce8..5b34508 100644 --- a/chat/room.go +++ b/chat/room.go @@ -226,7 +226,7 @@ func (r *Room) SetTopic(s string) { // NamesPrefix lists all members' names with a given prefix, used to query // for autocompletion purposes. Sorted by which user was last active. -func (r *Room) NamesPrefix(prefix string, current_user *message.User) []string { +func (r *Room) NamesPrefix(prefix string) []string { items := r.Members.ListPrefix(prefix) // Sort results by recently active @@ -235,14 +235,6 @@ func (r *Room) NamesPrefix(prefix string, current_user *message.User) []string { users = append(users, item.Value().(*Member).User) } sort.Sort(message.RecentActiveUsers(users)) - for i, user := range users { - if user.Name() == current_user.Name() { - // move it to the end. one user in the list? - save := users[0] - copy(users[i:], users[i+1:]) - users[len(users)-1] = save - } - } // Pull out names names := make([]string, 0, len(items)) diff --git a/chat/room_test.go b/chat/room_test.go index 110c033..c316e15 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -394,20 +394,20 @@ func TestRoomNamesPrefix(t *testing.T) { members[3].HandleMsg(message.NewMsg("hi")) // foo members[1].HandleMsg(message.NewMsg("hi")) // aab - if got, want := r.NamesPrefix("a", members[3].User), []string{"aab", "aaa", "aac"}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("a"), []string{"aab", "aaa", "aac"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } members[2].HandleMsg(message.NewMsg("hi")) // aac - if got, want := r.NamesPrefix("a", members[3].User), []string{"aac", "aab", "aaa"}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("a"), []string{"aac", "aab", "aaa"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } - if got, want := r.NamesPrefix("f", members[0].User), []string{"foo"}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("f"), []string{"foo"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } - if got, want := r.NamesPrefix("bar", members[3].User), []string{}; !reflect.DeepEqual(got, want) { + if got, want := r.NamesPrefix("bar"), []string{}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } } diff --git a/host.go b/host.go index 808d9fc..7be6aed 100644 --- a/host.go +++ b/host.go @@ -243,14 +243,19 @@ func (h *Host) Serve() { h.listener.Serve() } -func (h *Host) completeName(partial string, current_user *message.User) string { - names := h.NamesPrefix(partial, current_user) +func (h *Host) completeName(partial string, skipName string) string { + names := h.NamesPrefix(partial) if len(names) == 0 { // Didn't find anything return "" + } else if name := names[0]; name != skipName { + // First name is not the skipName, great + return name + } else if len(names) > 1 { + // Next candidate + return names[1] } - - return names[len(names)-1] + return "" } func (h *Host) completeCommand(partial string) string { @@ -300,7 +305,7 @@ func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int, } } else { // Name - completed = h.completeName(partial, u) + completed = h.completeName(partial, u.Name()) if completed == "" { return } From 7822904afc143bd42bee79a5dbefb5924f727caa Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 30 Jul 2020 12:02:07 -0400 Subject: [PATCH 07/12] chat/message: Fix RecentActiveUsers sort order --- chat/message/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat/message/user.go b/chat/message/user.go index 3f96e28..d4cc304 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -262,7 +262,7 @@ func (a RecentActiveUsers) Less(i, j int) bool { if a[i].lastMsg.IsZero() { return a[i].joined.Before(a[j].joined) } else { - return a[i].lastMsg.After(a[j].lastMsg) + return a[i].lastMsg.Before(a[j].lastMsg) } } From 86b70a1fc7059dd189720b6b4086f6309ec720bd Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 30 Jul 2020 12:05:38 -0400 Subject: [PATCH 08/12] chat: go fmt --- chat/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat/command.go b/chat/command.go index fe9355e..1869685 100644 --- a/chat/command.go +++ b/chat/command.go @@ -5,9 +5,9 @@ package chat import ( "errors" "fmt" + "sort" "strings" "time" - "sort" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/internal/sanitize" From 987a2e870af472f169475fadea4a7baf4585d9ed Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 30 Jul 2020 12:52:32 -0400 Subject: [PATCH 09/12] chat/message: Set LastMsg during render of self public messages, fix sorting Also fixed chat tests --- chat/message/message.go | 3 +++ chat/message/user.go | 30 ++++++++++++++++++++++-------- chat/room_test.go | 15 ++++++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/chat/message/message.go b/chat/message/message.go index 01de0ce..79244eb 100644 --- a/chat/message/message.go +++ b/chat/message/message.go @@ -131,6 +131,9 @@ func (m PublicMsg) RenderFor(cfg UserConfig) string { // RenderSelf renders the message for when it's echoing your own message. func (m PublicMsg) RenderSelf(cfg UserConfig) string { + if cfg.Theme == nil { + return fmt.Sprintf("[%s] %s", m.from.Name(), m.body) + } return fmt.Sprintf("[%s] %s", cfg.Theme.ColorName(m.from), m.body) } diff --git a/chat/message/user.go b/chat/message/user.go index d4cc304..8e522d2 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -62,6 +62,12 @@ func (u *User) Joined() time.Time { return u.joined } +func (u *User) LastMsg() time.Time { + u.mu.Lock() + defer u.mu.Unlock() + return u.lastMsg +} + func (u *User) Config() UserConfig { u.mu.Lock() defer u.mu.Unlock() @@ -161,6 +167,10 @@ func (u *User) render(m Message) string { switch m := m.(type) { case PublicMsg: if u == m.From() { + u.mu.Lock() + u.lastMsg = m.Timestamp() + u.mu.Unlock() + if !cfg.Echo { return "" } @@ -204,9 +214,6 @@ func (u *User) writeMsg(m Message) error { // HandleMsg will render the message to the screen, blocking. func (u *User) HandleMsg(m Message) error { - u.mu.Lock() - u.lastMsg = m.Timestamp() - u.mu.Unlock() return u.writeMsg(m) } @@ -248,7 +255,9 @@ func init() { // TODO: Seed random? } -// RecentActiveUsers is a slice of *Users that knows how to be sorted by the time of the last message. +// RecentActiveUsers is a slice of *Users that knows how to be sorted by the +// time of the last message. If no message has been sent, then fall back to the +// time joined instead. type RecentActiveUsers []*User func (a RecentActiveUsers) Len() int { return len(a) } @@ -259,10 +268,15 @@ func (a RecentActiveUsers) Less(i, j int) bool { a[j].mu.Lock() defer a[j].mu.Unlock() - if a[i].lastMsg.IsZero() { - return a[i].joined.Before(a[j].joined) - } else { - return a[i].lastMsg.Before(a[j].lastMsg) + ai := a[i].lastMsg + if ai.IsZero() { + ai = a[i].joined } + aj := a[j].lastMsg + if aj.IsZero() { + aj = a[j].joined + } + + return ai.After(aj) } diff --git a/chat/room_test.go b/chat/room_test.go index c316e15..44c8f0c 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -388,17 +388,22 @@ func TestRoomNamesPrefix(t *testing.T) { } } + sendMsg := func(from *Member, body string) { + // lastMsg is set during render of self messags, so we can't use NewMsg + from.HandleMsg(message.NewPublicMsg(body, from.User)) + } + // Inject some activity - members[2].HandleMsg(message.NewMsg("hi")) // aac - members[0].HandleMsg(message.NewMsg("hi")) // aaa - members[3].HandleMsg(message.NewMsg("hi")) // foo - members[1].HandleMsg(message.NewMsg("hi")) // aab + sendMsg(members[2], "hi") // aac + sendMsg(members[0], "hi") // aaa + sendMsg(members[3], "hi") // foo + sendMsg(members[1], "hi") // aab if got, want := r.NamesPrefix("a"), []string{"aab", "aaa", "aac"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } - members[2].HandleMsg(message.NewMsg("hi")) // aac + sendMsg(members[2], "hi") // aac if got, want := r.NamesPrefix("a"), []string{"aac", "aab", "aaa"}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q; want: %q", got, want) } From a9b08a7b17628e96c3e47023f19d85e919e9bd5e Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 30 Jul 2020 13:10:41 -0400 Subject: [PATCH 10/12] /whois: Add extra room info for admins Will need to add room context to non-admins eventually too --- identity.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/identity.go b/identity.go index 93df4e5..5eb074b 100644 --- a/identity.go +++ b/identity.go @@ -2,6 +2,7 @@ package sshchat import ( "net" + "strings" "time" "github.com/shazow/ssh-chat/chat" @@ -48,6 +49,7 @@ func (i Identity) Name() string { } // Whois returns a whois description for non-admin users. +// TODO: Add optional room context? func (i Identity) Whois() string { fingerprint := "(no public key)" if i.PublicKey() != nil { @@ -67,15 +69,24 @@ func (i Identity) WhoisAdmin(room *chat.Room) string { fingerprint = sshd.Fingerprint(i.PublicKey()) } - isOp := "" - if member, ok := room.MemberByID(i.ID()); ok && room.IsOp(member.User) { - isOp = message.Newline + " > op: true" - } - - return "name: " + i.Name() + message.Newline + + out := strings.Builder{} + out.WriteString("name: " + i.Name() + message.Newline + " > ip: " + ip + message.Newline + " > fingerprint: " + fingerprint + message.Newline + " > client: " + sanitize.Data(string(i.ClientVersion()), 64) + message.Newline + - " > joined: " + humantime.Since(i.created) + " ago" + - isOp + " > joined: " + humantime.Since(i.created) + " ago") + + if member, ok := room.MemberByID(i.ID()); ok { + // Add room-specific whois + // FIXME: Should these always be present, even if they're false? Maybe + // change that once we add room context to Whois() above. + if !member.LastMsg().IsZero() { + out.WriteString(message.Newline + " > room/messaged: " + humantime.Since(member.LastMsg()) + " ago") + } + if room.IsOp(member.User) { + out.WriteString(message.Newline + " > room/op: true") + } + } + + return out.String() } From 8cd06e33b5f13af73b2d859942224bb2db9c4998 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 3 Aug 2020 11:32:16 -0400 Subject: [PATCH 11/12] set: Add Interface, ZeroValue helper --- set/set.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/set/set.go b/set/set.go index 92ef1bb..0b08e8c 100644 --- a/set/set.go +++ b/set/set.go @@ -15,6 +15,25 @@ var ErrMissing = errors.New("item does not exist") // Returned when a nil item is added. Nil values are considered expired and invalid. var ErrNil = errors.New("item value must not be nil") +// ZeroValue can be used when we only care about the key, not about the value. +var ZeroValue = struct{}{} + +// Interface is the Set interface +type Interface interface { + Clear() int + Each(fn IterFunc) error + // Add only if the item does not already exist + Add(item Item) error + // Set item, override if it already exists + Set(item Item) error + Get(key string) (Item, error) + In(key string) bool + Len() int + ListPrefix(prefix string) []Item + Remove(key string) error + Replace(oldKey string, item Item) error +} + type IterFunc func(key string, item Item) error type Set struct { From aa78d0eb221d825c72e49897cdefc4231b2b7541 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 3 Aug 2020 11:32:55 -0400 Subject: [PATCH 12/12] chat: Add /focus command Only show messages from focused users --- chat/command.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ chat/message/user.go | 7 ++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/chat/command.go b/chat/command.go index 1869685..db88011 100644 --- a/chat/command.go +++ b/chat/command.go @@ -411,4 +411,51 @@ func InitCommands(c *Commands) { return nil }, }) + + c.Add(Command{ + Prefix: "/focus", + PrefixHelp: "[USER ...]", + Help: "Only show messages from focused users, or $ to reset.", + Handler: func(room *Room, msg message.CommandMsg) error { + ids := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/focus")) + if ids == "" { + // Print focused names, if any. + var names []string + msg.From().Focused.Each(func(_ string, item set.Item) error { + names = append(names, item.Key()) + return nil + }) + + var systemMsg string + if len(names) == 0 { + systemMsg = "Unfocused." + } else { + systemMsg = fmt.Sprintf("Focusing on %d users: %s", len(names), strings.Join(names, ", ")) + } + + room.Send(message.NewSystemMsg(systemMsg, msg.From())) + return nil + } + + n := msg.From().Focused.Clear() + if ids == "$" { + room.Send(message.NewSystemMsg(fmt.Sprintf("Removed focus from %d users.", n), msg.From())) + return nil + } + + var focused []string + for _, name := range strings.Split(ids, " ") { + id := sanitize.Name(name) + if id == "" { + continue // Skip + } + focused = append(focused, id) + if err := msg.From().Focused.Set(set.Itemize(id, set.ZeroValue)); err != nil { + return err + } + } + room.Send(message.NewSystemMsg(fmt.Sprintf("Focusing: %s", strings.Join(focused, ", ")), msg.From())) + return nil + }, + }) } diff --git a/chat/message/user.go b/chat/message/user.go index 8e522d2..d1d862f 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -22,7 +22,8 @@ var ErrUserClosed = errors.New("user closed") // User definition, implemented set Item interface and io.Writer type User struct { Identifier - Ignored *set.Set + Ignored set.Interface + Focused set.Interface colorIdx int joined time.Time msg chan Message @@ -45,6 +46,7 @@ func NewUser(identity Identifier) *User { msg: make(chan Message, messageBuffer), done: make(chan struct{}), Ignored: set.New(), + Focused: set.New(), } u.setColorIdx(rand.Int()) @@ -175,6 +177,9 @@ func (u *User) render(m Message) string { return "" } out += m.RenderSelf(cfg) + } else if u.Focused.Len() > 0 && !u.Focused.In(m.From().ID()) { + // Skip message during focus + return "" } else { out += m.RenderFor(cfg) }