mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-05-16 06:56:36 +03:00
- Added new rank: Administators (or masters). This rank can add or delete operators, but cannot add or remove another administrators. Administrators rank is designed by public key on server startup. - Added "vim mode". With this mode you can use commands as ':' instead of '/'. As you can see in the code, the Prefix parameter of Command structure was changed by a neutral parameter for future introductions of prefixes. - New Help function. This function add help for admin rank. - Added the following commands: - setnick: Administrators can change the nick of another user. - private: Users can stablish privates conversations. This conversations are permanent until they execute endprivate command. - endprivate: Finish private conversation. - welcome: Prints motd. Only admins can execute this. - whois: In whois only admins can see ip. - delop: Allow admins to delete an operator (or moderator as is mentioned in some parts of the code). - Changed historyLen and roomBuffer vars. - In cmd tool have been added fsnotify to update public key files (admin and moderator public key files). This is to prevent to restart the server everytime we want to add an administrator. - Added new prompt mode. In this mode you can see in which room you are talking. As default is 'general'. If you start private conversation the room will be the name of the another user. This part has been introduced for future implementations. - As I said before now exists private conversations (unlike msg command).
384 lines
9.0 KiB
Go
384 lines
9.0 KiB
Go
package chat
|
|
|
|
// FIXME: Would be sweet if we could piggyback on a cli parser or something.
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"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")
|
|
|
|
// 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
|
|
// command requires Admin permissions
|
|
Admin 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()[1:]]
|
|
if !ok {
|
|
return ErrInvalidCommand
|
|
}
|
|
|
|
return cmd.Handler(room, msg)
|
|
}
|
|
|
|
// Help will return collated help text as one string.
|
|
func (c Commands) Help(showOp, showAdmin bool) string {
|
|
// Filter by op
|
|
op := []*Command{}
|
|
admin := []*Command{}
|
|
normal := []*Command{}
|
|
for _, cmd := range c {
|
|
if cmd.Op {
|
|
op = append(op, cmd)
|
|
}
|
|
if cmd.Admin {
|
|
admin = append(admin, cmd)
|
|
}
|
|
if !cmd.Op && !cmd.Admin {
|
|
normal = append(normal, cmd)
|
|
}
|
|
}
|
|
help := "Available commands:" + message.Newline + NewCommandsHelp(normal).String()
|
|
if showOp {
|
|
help += message.Newline + "-> Operator commands:" + message.Newline + NewCommandsHelp(op).String()
|
|
}
|
|
if showAdmin {
|
|
help += message.Newline + "-> Admin commands:" + message.Newline + NewCommandsHelp(admin).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())
|
|
admin := room.IsMaster(msg.From())
|
|
room.Send(message.NewSystemMsg(room.commands.Help(op, admin), 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 {
|
|
msg.From().Close()
|
|
return nil
|
|
},
|
|
})
|
|
c.Alias("exit", "quit")
|
|
c.Alias("q", "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{
|
|
Admin: true,
|
|
Prefix: "setnick",
|
|
PrefixHelp: "NAME ANOTHER",
|
|
Help: "Rename NAME to ANOTHER username",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
args := msg.Args()
|
|
if len(args) < 2 {
|
|
return errors.New("invalid arguments")
|
|
}
|
|
|
|
member, ok := room.MemberByID(args[0])
|
|
if !ok {
|
|
return errors.New("failed to find member")
|
|
}
|
|
|
|
oldID := member.ID()
|
|
newID := SanitizeName(args[1])
|
|
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().Config().Theme
|
|
|
|
colorize := func(u *message.User) string {
|
|
return theme.ColorName(u)
|
|
}
|
|
|
|
if theme == nil {
|
|
colorize = func(u *message.User) string {
|
|
return u.Name()
|
|
}
|
|
}
|
|
|
|
names := room.Members.ListPrefix("")
|
|
colNames := make([]string, len(names))
|
|
for i, uname := range names {
|
|
colNames[i] = colorize(uname.Value().(*Member).User)
|
|
}
|
|
|
|
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()
|
|
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()
|
|
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: "ignore",
|
|
PrefixHelp: "[USER]",
|
|
Help: "Hide messages from USER, /unignore USER to stop hiding.",
|
|
Handler: func(room *Room, msg message.CommandMsg) error {
|
|
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/ignore"))
|
|
if id == "" {
|
|
// Print ignored names, if any.
|
|
var names []string
|
|
msg.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 := msg.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")
|
|
}
|
|
|
|
if err := msg.From().Ignored.Remove(id); err != nil {
|
|
return err
|
|
}
|
|
|
|
room.Send(message.NewSystemMsg(fmt.Sprintf("No longer ignoring: %s", id), msg.From()))
|
|
return nil
|
|
},
|
|
})
|
|
}
|