mirror of
				https://github.com/shazow/ssh-chat.git
				synced 2025-11-04 05:25:33 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			413 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			413 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package chat
 | 
						|
 | 
						|
// FIXME: Would be sweet if we could piggyback on a cli parser or something.
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/shazow/ssh-chat/chat/message"
 | 
						|
	"github.com/shazow/ssh-chat/internal/sanitize"
 | 
						|
	"github.com/shazow/ssh-chat/set"
 | 
						|
)
 | 
						|
 | 
						|
// ErrInvalidCommand is the error returned when an invalid command is issued.
 | 
						|
var ErrInvalidCommand = errors.New("invalid command")
 | 
						|
 | 
						|
// ErrNoOwner is the error returned when a command is given without an owner.
 | 
						|
var ErrNoOwner = errors.New("command without owner")
 | 
						|
 | 
						|
// ErrMissingArg is the error returned when a command is performed without the necessary
 | 
						|
// number of arguments.
 | 
						|
var ErrMissingArg = errors.New("missing argument")
 | 
						|
 | 
						|
// ErrMissingPrefix is 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 {
 | 
						|
	Prefix     string // The command's key, such as /foo
 | 
						|
	PrefixHelp string // Extra help regarding arguments
 | 
						|
	Help       string // help text, if omitted, command is hidden from /help
 | 
						|
	Op         bool   // does the command require Op permissions?
 | 
						|
 | 
						|
	// Handler for the command
 | 
						|
	Handler func(*Room, message.CommandMsg) error
 | 
						|
}
 | 
						|
 | 
						|
// 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 timeformatDatetime = "2006-01-02 15:04:05"
 | 
						|
 | 
						|
var timeformatTime = "15:04"
 | 
						|
 | 
						|
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 {
 | 
						|
			msg.From().Close()
 | 
						|
			return nil
 | 
						|
		},
 | 
						|
	})
 | 
						|
	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 := sanitize.Name(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().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 strings.Builder
 | 
						|
				fmt.Fprintf(&output, "Current theme: %s%s", theme, message.Newline)
 | 
						|
				fmt.Fprintf(&output, "   Themes available: ")
 | 
						|
 | 
						|
				for i, t := range message.Themes {
 | 
						|
					output.WriteString(t.ID())
 | 
						|
					if i < len(message.Themes)-1 {
 | 
						|
						output.WriteString(", ")
 | 
						|
					}
 | 
						|
				}
 | 
						|
				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: "/shrug",
 | 
						|
		Handler: func(room *Room, msg message.CommandMsg) error {
 | 
						|
			room.Send(message.NewEmoteMsg(`¯\_(ツ)_/¯`, msg.From()))
 | 
						|
			return nil
 | 
						|
		},
 | 
						|
	})
 | 
						|
 | 
						|
	c.Add(Command{
 | 
						|
		Prefix:     "/timestamp",
 | 
						|
		PrefixHelp: "[time|datetime]",
 | 
						|
		Help:       "Prefix messages with a timestamp. You can also provide the UTC offset: /timestamp time +5h45m",
 | 
						|
		Handler: func(room *Room, msg message.CommandMsg) error {
 | 
						|
			u := msg.From()
 | 
						|
			cfg := u.Config()
 | 
						|
 | 
						|
			args := msg.Args()
 | 
						|
			mode := ""
 | 
						|
			if len(args) >= 1 {
 | 
						|
				mode = args[0]
 | 
						|
			}
 | 
						|
			if len(args) >= 2 {
 | 
						|
				// FIXME: This is an annoying format to demand from users, but
 | 
						|
				// hopefully we can make it a non-primary flow if we add GeoIP
 | 
						|
				// someday.
 | 
						|
				offset, err := time.ParseDuration(args[1])
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				cfg.Timezone = time.FixedZone("", int(offset.Seconds()))
 | 
						|
			}
 | 
						|
 | 
						|
			switch mode {
 | 
						|
			case "time":
 | 
						|
				cfg.Timeformat = &timeformatTime
 | 
						|
			case "datetime":
 | 
						|
				cfg.Timeformat = &timeformatDatetime
 | 
						|
			case "":
 | 
						|
				// Toggle
 | 
						|
				if cfg.Timeformat != nil {
 | 
						|
					cfg.Timeformat = nil
 | 
						|
				} else {
 | 
						|
					cfg.Timeformat = &timeformatTime
 | 
						|
				}
 | 
						|
			case "off":
 | 
						|
				cfg.Timeformat = nil
 | 
						|
			default:
 | 
						|
				return errors.New("timestamp value must be one of: time, datetime, off")
 | 
						|
			}
 | 
						|
 | 
						|
			u.SetConfig(cfg)
 | 
						|
 | 
						|
			var body string
 | 
						|
			if cfg.Timeformat != nil {
 | 
						|
				if cfg.Timezone != nil {
 | 
						|
					tzname := time.Now().In(cfg.Timezone).Format("MST")
 | 
						|
					body = fmt.Sprintf("Timestamp is toggled ON, timezone is %q", tzname)
 | 
						|
				} else {
 | 
						|
					body = "Timestamp is toggled ON, timezone is UTC"
 | 
						|
				}
 | 
						|
			} 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 {
 | 
						|
			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
 | 
						|
		},
 | 
						|
	})
 | 
						|
}
 |