mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-13 07:37:17 +03:00
Merge branch 'master' into sponsor-prefix
This commit is contained in:
commit
e0ab53500e
@ -5,6 +5,7 @@ package chat
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -189,6 +190,7 @@ func InitCommands(c *Commands) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
names := room.Members.ListPrefix("")
|
names := room.Members.ListPrefix("")
|
||||||
|
sort.Slice(names, func(i, j int) bool { return names[i].Key() < names[j].Key() })
|
||||||
colNames := make([]string, len(names))
|
colNames := make([]string, len(names))
|
||||||
for i, uname := range names {
|
for i, uname := range names {
|
||||||
colNames[i] = colorize(uname.Value().(*Member).User)
|
colNames[i] = colorize(uname.Value().(*Member).User)
|
||||||
@ -409,4 +411,51 @@ func InitCommands(c *Commands) {
|
|||||||
return nil
|
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
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,9 @@ func (m PublicMsg) RenderFor(cfg UserConfig) string {
|
|||||||
|
|
||||||
// RenderSelf renders the message for when it's echoing your own message.
|
// RenderSelf renders the message for when it's echoing your own message.
|
||||||
func (m PublicMsg) RenderSelf(cfg UserConfig) string {
|
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)
|
return fmt.Sprintf("[%s] %s", cfg.Theme.ColorName(m.from), m.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ var ErrUserClosed = errors.New("user closed")
|
|||||||
// User definition, implemented set Item interface and io.Writer
|
// User definition, implemented set Item interface and io.Writer
|
||||||
type User struct {
|
type User struct {
|
||||||
Identifier
|
Identifier
|
||||||
Ignored *set.Set
|
|
||||||
OnChange func()
|
OnChange func()
|
||||||
|
Ignored set.Interface
|
||||||
|
Focused set.Interface
|
||||||
colorIdx int
|
colorIdx int
|
||||||
joined time.Time
|
joined time.Time
|
||||||
msg chan Message
|
msg chan Message
|
||||||
@ -47,6 +47,7 @@ func NewUser(identity Identifier) *User {
|
|||||||
msg: make(chan Message, messageBuffer),
|
msg: make(chan Message, messageBuffer),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
Ignored: set.New(),
|
Ignored: set.New(),
|
||||||
|
Focused: set.New(),
|
||||||
}
|
}
|
||||||
u.setColorIdx(rand.Int())
|
u.setColorIdx(rand.Int())
|
||||||
|
|
||||||
@ -64,6 +65,12 @@ func (u *User) Joined() time.Time {
|
|||||||
return u.joined
|
return u.joined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) LastMsg() time.Time {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
return u.lastMsg
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) Config() UserConfig {
|
func (u *User) Config() UserConfig {
|
||||||
u.mu.Lock()
|
u.mu.Lock()
|
||||||
defer u.mu.Unlock()
|
defer u.mu.Unlock()
|
||||||
@ -171,10 +178,17 @@ func (u *User) render(m Message) string {
|
|||||||
switch m := m.(type) {
|
switch m := m.(type) {
|
||||||
case PublicMsg:
|
case PublicMsg:
|
||||||
if u == m.From() {
|
if u == m.From() {
|
||||||
|
u.mu.Lock()
|
||||||
|
u.lastMsg = m.Timestamp()
|
||||||
|
u.mu.Unlock()
|
||||||
|
|
||||||
if !cfg.Echo {
|
if !cfg.Echo {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
out += m.RenderSelf(cfg)
|
out += m.RenderSelf(cfg)
|
||||||
|
} else if u.Focused.Len() > 0 && !u.Focused.In(m.From().ID()) {
|
||||||
|
// Skip message during focus
|
||||||
|
return ""
|
||||||
} else {
|
} else {
|
||||||
out += m.RenderFor(cfg)
|
out += m.RenderFor(cfg)
|
||||||
}
|
}
|
||||||
@ -214,9 +228,6 @@ func (u *User) writeMsg(m Message) error {
|
|||||||
|
|
||||||
// HandleMsg will render the message to the screen, blocking.
|
// HandleMsg will render the message to the screen, blocking.
|
||||||
func (u *User) HandleMsg(m Message) error {
|
func (u *User) HandleMsg(m Message) error {
|
||||||
u.mu.Lock()
|
|
||||||
u.lastMsg = m.Timestamp()
|
|
||||||
u.mu.Unlock()
|
|
||||||
return u.writeMsg(m)
|
return u.writeMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +269,9 @@ func init() {
|
|||||||
// TODO: Seed random?
|
// 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
|
type RecentActiveUsers []*User
|
||||||
|
|
||||||
func (a RecentActiveUsers) Len() int { return len(a) }
|
func (a RecentActiveUsers) Len() int { return len(a) }
|
||||||
@ -268,5 +281,16 @@ func (a RecentActiveUsers) Less(i, j int) bool {
|
|||||||
defer a[i].mu.Unlock()
|
defer a[i].mu.Unlock()
|
||||||
a[j].mu.Lock()
|
a[j].mu.Lock()
|
||||||
defer a[j].mu.Unlock()
|
defer a[j].mu.Unlock()
|
||||||
return a[i].lastMsg.After(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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
// Inject some activity
|
||||||
members[2].HandleMsg(message.NewMsg("hi")) // aac
|
sendMsg(members[2], "hi") // aac
|
||||||
members[0].HandleMsg(message.NewMsg("hi")) // aaa
|
sendMsg(members[0], "hi") // aaa
|
||||||
members[3].HandleMsg(message.NewMsg("hi")) // foo
|
sendMsg(members[3], "hi") // foo
|
||||||
members[1].HandleMsg(message.NewMsg("hi")) // aab
|
sendMsg(members[1], "hi") // aab
|
||||||
|
|
||||||
if got, want := r.NamesPrefix("a"), []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)
|
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) {
|
if got, want := r.NamesPrefix("a"), []string{"aac", "aab", "aaa"}; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("got: %q; want: %q", got, want)
|
t.Errorf("got: %q; want: %q", got, want)
|
||||||
}
|
}
|
||||||
|
16
host.go
16
host.go
@ -237,14 +237,19 @@ func (h *Host) Serve() {
|
|||||||
h.listener.Serve()
|
h.listener.Serve()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Host) completeName(partial string) string {
|
func (h *Host) completeName(partial string, skipName string) string {
|
||||||
names := h.NamesPrefix(partial)
|
names := h.NamesPrefix(partial)
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
// Didn't find anything
|
// Didn't find anything
|
||||||
return ""
|
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 ""
|
||||||
return names[len(names)-1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Host) completeCommand(partial string) string {
|
func (h *Host) completeCommand(partial string) string {
|
||||||
@ -294,7 +299,7 @@ func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Name
|
// Name
|
||||||
completed = h.completeName(partial)
|
completed = h.completeName(partial, u.Name())
|
||||||
if completed == "" {
|
if completed == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -401,12 +406,11 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("user not found")
|
return errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := target.Identifier.(*Identity)
|
id := target.Identifier.(*Identity)
|
||||||
var whois string
|
var whois string
|
||||||
switch room.IsOp(msg.From()) {
|
switch room.IsOp(msg.From()) {
|
||||||
case true:
|
case true:
|
||||||
whois = id.WhoisAdmin()
|
whois = id.WhoisAdmin(room)
|
||||||
case false:
|
case false:
|
||||||
whois = id.Whois()
|
whois = id.Whois()
|
||||||
}
|
}
|
||||||
|
25
identity.go
25
identity.go
@ -2,8 +2,10 @@ package sshchat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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/internal/sanitize"
|
"github.com/shazow/ssh-chat/internal/sanitize"
|
||||||
@ -55,6 +57,7 @@ func (i Identity) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Whois returns a whois description for non-admin users.
|
// Whois returns a whois description for non-admin users.
|
||||||
|
// TODO: Add optional room context?
|
||||||
func (i Identity) Whois() string {
|
func (i Identity) Whois() string {
|
||||||
fingerprint := "(no public key)"
|
fingerprint := "(no public key)"
|
||||||
if i.PublicKey() != nil {
|
if i.PublicKey() != nil {
|
||||||
@ -67,15 +70,31 @@ func (i Identity) Whois() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WhoisAdmin returns a whois description for admin users.
|
// WhoisAdmin returns a whois description for admin users.
|
||||||
func (i Identity) WhoisAdmin() string {
|
func (i Identity) WhoisAdmin(room *chat.Room) string {
|
||||||
ip, _, _ := net.SplitHostPort(i.RemoteAddr().String())
|
ip, _, _ := net.SplitHostPort(i.RemoteAddr().String())
|
||||||
fingerprint := "(no public key)"
|
fingerprint := "(no public key)"
|
||||||
if i.PublicKey() != nil {
|
if i.PublicKey() != nil {
|
||||||
fingerprint = sshd.Fingerprint(i.PublicKey())
|
fingerprint = sshd.Fingerprint(i.PublicKey())
|
||||||
}
|
}
|
||||||
return "name: " + i.Name() + message.Newline +
|
|
||||||
|
out := strings.Builder{}
|
||||||
|
out.WriteString("name: " + i.Name() + message.Newline +
|
||||||
" > ip: " + ip + message.Newline +
|
" > ip: " + ip + message.Newline +
|
||||||
" > fingerprint: " + fingerprint + message.Newline +
|
" > fingerprint: " + fingerprint + message.Newline +
|
||||||
" > client: " + sanitize.Data(string(i.ClientVersion()), 64) + message.Newline +
|
" > client: " + sanitize.Data(string(i.ClientVersion()), 64) + message.Newline +
|
||||||
" > joined: " + humantime.Since(i.created) + " ago"
|
" > 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()
|
||||||
}
|
}
|
||||||
|
19
set/set.go
19
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.
|
// Returned when a nil item is added. Nil values are considered expired and invalid.
|
||||||
var ErrNil = errors.New("item value must not be nil")
|
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 IterFunc func(key string, item Item) error
|
||||||
|
|
||||||
type Set struct {
|
type Set struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user