soju/service.go
delthas 7f74055380 Add support for custom network on-connect commands
Some servers use custom IRC bots with custom commands for registering to
specific services after connection.

This adds support for setting custom raw IRC messages, that will be
sent after registering to a network.

It also adds support for a custom flag.Value type for string
slice flags (flags taking several string values).
2020-04-16 17:38:47 +02:00

301 lines
6.9 KiB
Go

package soju
import (
"flag"
"fmt"
"io/ioutil"
"strings"
"github.com/google/shlex"
"golang.org/x/crypto/bcrypt"
"gopkg.in/irc.v3"
)
const serviceNick = "BouncerServ"
var servicePrefix = &irc.Prefix{
Name: serviceNick,
User: serviceNick,
Host: serviceNick,
}
type serviceCommandSet map[string]*serviceCommand
type serviceCommand struct {
usage string
desc string
handle func(dc *downstreamConn, params []string) error
children serviceCommandSet
}
func sendServiceNOTICE(dc *downstreamConn, text string) {
dc.SendMessage(&irc.Message{
Prefix: servicePrefix,
Command: "NOTICE",
Params: []string{dc.nick, text},
})
}
func sendServicePRIVMSG(dc *downstreamConn, text string) {
dc.SendMessage(&irc.Message{
Prefix: servicePrefix,
Command: "PRIVMSG",
Params: []string{dc.nick, text},
})
}
func handleServicePRIVMSG(dc *downstreamConn, text string) {
words, err := shlex.Split(text)
if err != nil {
sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
return
}
cmd, params, err := serviceCommands.Get(words)
if err != nil {
sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
return
}
if err := cmd.handle(dc, params); err != nil {
sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
}
}
func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
if len(params) == 0 {
return nil, nil, fmt.Errorf("no command specified")
}
name := params[0]
params = params[1:]
cmd, ok := cmds[name]
if !ok {
for k := range cmds {
if !strings.HasPrefix(k, name) {
continue
}
if cmd != nil {
return nil, params, fmt.Errorf("command %q is ambiguous", name)
}
cmd = cmds[k]
}
}
if cmd == nil {
return nil, params, fmt.Errorf("command %q not found", name)
}
if len(params) == 0 || len(cmd.children) == 0 {
return cmd, params, nil
}
return cmd.children.Get(params)
}
var serviceCommands serviceCommandSet
func init() {
serviceCommands = serviceCommandSet{
"help": {
usage: "[command]",
desc: "print help message",
handle: handleServiceHelp,
},
"network": {
children: serviceCommandSet{
"create": {
usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [[-connect-command command] ...]",
desc: "add a new network",
handle: handleServiceCreateNetwork,
},
"status": {
desc: "show a list of saved networks and their current status",
handle: handleServiceNetworkStatus,
},
"delete": {
usage: "<name>",
desc: "delete a network",
handle: handleServiceNetworkDelete,
},
},
},
"change-password": {
usage: "<new password>",
desc: "change your password",
handle: handlePasswordChange,
},
}
}
func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, l *[]string) {
for name, cmd := range cmds {
words := append(prefix, name)
if len(cmd.children) == 0 {
s := strings.Join(words, " ")
*l = append(*l, s)
} else {
appendServiceCommandSetHelp(cmd.children, words, l)
}
}
}
func handleServiceHelp(dc *downstreamConn, params []string) error {
if len(params) > 0 {
cmd, rest, err := serviceCommands.Get(params)
if err != nil {
return err
}
words := params[:len(params)-len(rest)]
if len(cmd.children) > 0 {
var l []string
appendServiceCommandSetHelp(cmd.children, words, &l)
sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
} else {
text := strings.Join(words, " ")
if cmd.usage != "" {
text += " " + cmd.usage
}
text += ": " + cmd.desc
sendServicePRIVMSG(dc, text)
}
} else {
var l []string
appendServiceCommandSetHelp(serviceCommands, nil, &l)
sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
}
return nil
}
func newFlagSet() *flag.FlagSet {
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.SetOutput(ioutil.Discard)
return fs
}
type stringSliceVar []string
func (v *stringSliceVar) String() string {
return fmt.Sprint([]string(*v))
}
func (v *stringSliceVar) Set(s string) error {
*v = append(*v, s)
return nil
}
func handleServiceCreateNetwork(dc *downstreamConn, params []string) error {
fs := newFlagSet()
addr := fs.String("addr", "", "")
name := fs.String("name", "", "")
username := fs.String("username", "", "")
pass := fs.String("pass", "", "")
realname := fs.String("realname", "", "")
nick := fs.String("nick", "", "")
var connectCommands stringSliceVar
fs.Var(&connectCommands, "connect-command", "")
if err := fs.Parse(params); err != nil {
return err
}
if *addr == "" {
return fmt.Errorf("flag -addr is required")
}
for _, command := range connectCommands {
_, err := irc.ParseMessage(command)
if err != nil {
return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
}
}
if *nick == "" {
*nick = dc.nick
}
var err error
network, err := dc.user.createNetwork(&Network{
Addr: *addr,
Name: *name,
Username: *username,
Pass: *pass,
Realname: *realname,
Nick: *nick,
ConnectCommands: connectCommands,
})
if err != nil {
return fmt.Errorf("could not create network: %v", err)
}
sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
return nil
}
func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
dc.user.forEachNetwork(func(net *network) {
var statuses []string
var details string
if uc := net.upstream(); uc != nil {
statuses = append(statuses, "connected as "+uc.nick)
details = fmt.Sprintf("%v channels", len(uc.channels))
} else {
statuses = append(statuses, "disconnected")
if net.lastError != nil {
details = net.lastError.Error()
}
}
if net == dc.network {
statuses = append(statuses, "current")
}
name := net.GetName()
if name != net.Addr {
name = fmt.Sprintf("%v (%v)", name, net.Addr)
}
s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
if details != "" {
s += ": " + details
}
sendServicePRIVMSG(dc, s)
})
return nil
}
func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
if len(params) != 1 {
return fmt.Errorf("expected exactly one argument")
}
net := dc.user.getNetwork(params[0])
if net == nil {
return fmt.Errorf("unknown network %q", params[0])
}
if err := dc.user.deleteNetwork(net.ID); err != nil {
return err
}
sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
return nil
}
func handlePasswordChange(dc *downstreamConn, params []string) error {
if len(params) != 1 {
return fmt.Errorf("expected exactly one argument")
}
hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %v", err)
}
if err := dc.user.updatePassword(string(hashed)); err != nil {
return err
}
sendServicePRIVMSG(dc, "password updated")
return nil
}