mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-12 15:17:16 +03:00
388 lines
9.1 KiB
Go
388 lines
9.1 KiB
Go
package chat
|
|
|
|
// FIXME: Would be sweet if we could piggyback on a cli parser or something.
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/shazow/ssh-chat/chat/message"
|
|
"github.com/shazow/ssh-chat/set"
|
|
)
|
|
|
|
// The error returned when an invalid command is issued.
|
|
var ErrInvalidCommand = errors.New("invalid command")
|
|
|
|
// The error returned when a command is given without an owner.
|
|
var ErrNoOwner = errors.New("command without owner")
|
|
|
|
// The error returned when a command is performed without the necessary number
|
|
// of arguments.
|
|
var ErrMissingArg = errors.New("missing argument")
|
|
|
|
// The error returned when a command is added without a prefix.
|
|
var ErrMissingPrefix = errors.New("command missing prefix")
|
|
|
|
// The error returned when we fail to find a corresponding userMember struct
|
|
// for an ID. This should not happen, probably a bug somewhere if encountered.
|
|
var ErrMissingMember = errors.New("failed to find member")
|
|
|
|
// Command is a definition of a handler for a command.
|
|
type Command struct {
|
|
// The command's key, such as /foo
|
|
Prefix string
|
|
// Extra help regarding arguments
|
|
PrefixHelp string
|
|
// If omitted, command is hidden from /help
|
|
Help string
|
|
Handler func(*Room, message.CommandMsg) error
|
|
// Command requires Op permissions
|
|
Op bool
|
|
}
|
|
|
|
// Commands is a registry of available commands.
|
|
type Commands map[string]*Command
|
|
|
|
// Add will register a command. If help string is empty, it will be hidden from
|
|
// Help().
|
|
func (c Commands) Add(cmd Command) error {
|
|
if cmd.Prefix == "" {
|
|
return ErrMissingPrefix
|
|
}
|
|
|
|
c[cmd.Prefix] = &cmd
|
|
return nil
|
|
}
|
|
|
|
// Alias will add another command for the same handler, won't get added to help.
|
|
func (c Commands) Alias(command string, alias string) error {
|
|
cmd, ok := c[command]
|
|
if !ok {
|
|
return ErrInvalidCommand
|
|
}
|
|
c[alias] = cmd
|
|
return nil
|
|
}
|
|
|
|
// Run executes a command message.
|
|
func (c Commands) Run(room *Room, msg message.CommandMsg) error {
|
|
if msg.From() == nil {
|
|
return ErrNoOwner
|
|
}
|
|
|
|
cmd, ok := c[msg.Command()]
|
|
if !ok {
|
|
return ErrInvalidCommand
|
|
}
|
|
|
|
return cmd.Handler(room, msg)
|
|
}
|
|
|
|
// Help will return collated help text as one string.
|
|
func (c Commands) Help(showOp bool) string {
|
|
// Filter by op
|
|
op := []*Command{}
|
|
normal := []*Command{}
|
|
for _, cmd := range c {
|
|
if cmd.Op {
|
|
op = append(op, cmd)
|
|
} else {
|
|
normal = append(normal, cmd)
|
|
}
|
|
}
|
|
help := "Available commands:" + message.Newline + NewCommandsHelp(normal).String()
|
|
if showOp {
|
|
help += message.Newline + "-> Operator commands:" + message.Newline + NewCommandsHelp(op).String()
|
|
}
|
|
return help
|
|
}
|
|
|
|
var defaultCommands *Commands
|
|
|
|
func init() {
|
|
defaultCommands = &Commands{}
|
|
InitCommands(defaultCommands)
|
|
}
|
|
|
|
// InitCommands injects default commands into a Commands registry.
|
|
func InitCommands(c *Commands) {
|
|
c.Add(Command{
|
|
Prefix: "/help",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
op := room.IsOp(msg.From())
|
|
room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/me",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
me := strings.TrimLeft(msg.Body(), "/me")
|
|
if me == "" {
|
|
me = "is at a loss for words."
|
|
} else {
|
|
me = me[1:]
|
|
}
|
|
|
|
room.Send(message.NewEmoteMsg(me, msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/exit",
|
|
Help: "Exit the chat.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
return msg.From().(io.Closer).Close()
|
|
},
|
|
})
|
|
c.Alias("/exit", "/quit")
|
|
|
|
c.Add(Command{
|
|
Prefix: "/nick",
|
|
PrefixHelp: "NAME",
|
|
Help: "Rename yourself.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
args := msg.Args()
|
|
if len(args) != 1 {
|
|
return ErrMissingArg
|
|
}
|
|
u := msg.From()
|
|
|
|
member, ok := room.MemberByID(u.ID())
|
|
if !ok {
|
|
return errors.New("failed to find member")
|
|
}
|
|
|
|
oldID := member.ID()
|
|
newID := SanitizeName(args[0])
|
|
if newID == oldID {
|
|
return errors.New("new name is the same as the original")
|
|
}
|
|
member.SetID(newID)
|
|
err := room.Rename(oldID, member)
|
|
if err != nil {
|
|
member.SetID(oldID)
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/names",
|
|
Help: "List users who are connected.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
theme := msg.From().(Member).Config().Theme
|
|
|
|
colorize := func(u Member) string {
|
|
return theme.ColorName(u)
|
|
}
|
|
|
|
if theme == nil {
|
|
colorize = func(u Member) string {
|
|
return u.Name()
|
|
}
|
|
}
|
|
|
|
names := room.Members.ListPrefix("")
|
|
colNames := make([]string, len(names))
|
|
for i, uname := range names {
|
|
colNames[i] = colorize(uname.Value().(Member))
|
|
}
|
|
|
|
body := fmt.Sprintf("%d connected: %s", len(colNames), strings.Join(colNames, ", "))
|
|
room.Send(message.NewSystemMsg(body, msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
c.Alias("/names", "/list")
|
|
|
|
c.Add(Command{
|
|
Prefix: "/theme",
|
|
PrefixHelp: "[colors|...]",
|
|
Help: "Set your color theme.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
user := msg.From().(Member)
|
|
args := msg.Args()
|
|
cfg := user.Config()
|
|
if len(args) == 0 {
|
|
theme := "plain"
|
|
if cfg.Theme != nil {
|
|
theme = cfg.Theme.ID()
|
|
}
|
|
var output bytes.Buffer
|
|
fmt.Fprintf(&output, "Current theme: %s%s", theme, message.Newline)
|
|
fmt.Fprintf(&output, " Themes available: ")
|
|
for i, t := range message.Themes {
|
|
fmt.Fprintf(&output, t.ID())
|
|
if i < len(message.Themes)-1 {
|
|
fmt.Fprintf(&output, ", ")
|
|
}
|
|
}
|
|
room.Send(message.NewSystemMsg(output.String(), user))
|
|
return nil
|
|
}
|
|
|
|
id := args[0]
|
|
for _, t := range message.Themes {
|
|
if t.ID() == id {
|
|
cfg.Theme = &t
|
|
user.SetConfig(cfg)
|
|
body := fmt.Sprintf("Set theme: %s", id)
|
|
room.Send(message.NewSystemMsg(body, user))
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("theme not found")
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/quiet",
|
|
Help: "Silence room announcements.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
u := msg.From().(Member)
|
|
cfg := u.Config()
|
|
cfg.Quiet = !cfg.Quiet
|
|
u.SetConfig(cfg)
|
|
|
|
var body string
|
|
if cfg.Quiet {
|
|
body = "Quiet mode is toggled ON"
|
|
} else {
|
|
body = "Quiet mode is toggled OFF"
|
|
}
|
|
room.Send(message.NewSystemMsg(body, u))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/slap",
|
|
PrefixHelp: "NAME",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
var me string
|
|
args := msg.Args()
|
|
if len(args) == 0 {
|
|
me = "slaps themselves around a bit with a large trout."
|
|
} else {
|
|
me = fmt.Sprintf("slaps %s around a bit with a large trout.", strings.Join(args, " "))
|
|
}
|
|
|
|
room.Send(message.NewEmoteMsg(me, msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/shrug",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
var me string
|
|
args := msg.Args()
|
|
if len(args) == 0 {
|
|
me = `¯\_(ツ)_/¯`
|
|
}
|
|
|
|
room.Send(message.NewEmoteMsg(me, msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/timestamp",
|
|
Help: "Timestamps after 30min of inactivity.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
u := msg.From().(Member)
|
|
cfg := u.Config()
|
|
cfg.Timestamp = !cfg.Timestamp
|
|
u.SetConfig(cfg)
|
|
|
|
var body string
|
|
if cfg.Timestamp {
|
|
body = "Timestamp is toggled ON"
|
|
} else {
|
|
body = "Timestamp is toggled OFF"
|
|
}
|
|
room.Send(message.NewSystemMsg(body, u))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/ignore",
|
|
PrefixHelp: "[USER]",
|
|
Help: "Hide messages from USER, /unignore USER to stop hiding.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
from, ok := room.Member(msg.From())
|
|
if !ok {
|
|
return ErrMissingMember
|
|
}
|
|
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/ignore"))
|
|
if id == "" {
|
|
// Print ignored names, if any.
|
|
var names []string
|
|
from.Ignored.Each(func(_ string, item set.Item) error {
|
|
names = append(names, item.Key())
|
|
return nil
|
|
})
|
|
|
|
var systemMsg string
|
|
if len(names) == 0 {
|
|
systemMsg = "0 users ignored."
|
|
} else {
|
|
systemMsg = fmt.Sprintf("%d ignored: %s", len(names), strings.Join(names, ", "))
|
|
}
|
|
|
|
room.Send(message.NewSystemMsg(systemMsg, msg.From()))
|
|
return nil
|
|
}
|
|
|
|
if id == msg.From().ID() {
|
|
return errors.New("cannot ignore self")
|
|
}
|
|
target, ok := room.MemberByID(id)
|
|
if !ok {
|
|
return fmt.Errorf("user not found: %s", id)
|
|
}
|
|
|
|
err := from.Ignored.Add(set.Itemize(id, target))
|
|
if err == set.ErrCollision {
|
|
return fmt.Errorf("user already ignored: %s", id)
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
room.Send(message.NewSystemMsg(fmt.Sprintf("Ignoring: %s", target.Name()), msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
|
|
c.Add(Command{
|
|
Prefix: "/unignore",
|
|
PrefixHelp: "USER",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore"))
|
|
if id == "" {
|
|
return errors.New("must specify user")
|
|
}
|
|
|
|
from, ok := room.Member(msg.From())
|
|
if !ok {
|
|
return ErrMissingMember
|
|
}
|
|
|
|
if err := from.Ignored.Remove(id); err != nil {
|
|
return err
|
|
}
|
|
|
|
room.Send(message.NewSystemMsg(fmt.Sprintf("No longer ignoring: %s", id), msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
}
|