From 9284fe2c7c3a4bc4d7fd155e85739825520aba54 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 8 Nov 2020 12:12:24 +0100 Subject: [PATCH] add ctcp handlers, and improve destroyed handling. --- client.go | 27 ++++++++++++++++++- handlers/ctcp.go | 62 +++++++++++++++++++++++++++++++++++++++++++ handlers/mroleplay.go | 2 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 handlers/ctcp.go diff --git a/client.go b/client.go index 107c3f8..d78d322 100644 --- a/client.go +++ b/client.go @@ -60,6 +60,9 @@ var ErrTargetNotFound = errors.New("irc: target not found") // status target var ErrTargetIsStatus = errors.New("irc: cannot remove status target") +// ErrDestroyed is returned by Client.Connect if you try to connect a destroyed client. +var ErrDestroyed = errors.New("irc: client destroyed") + // A Client is an IRC client. You need to use New to construct it type Client struct { id string @@ -114,6 +117,8 @@ func New(ctx context.Context, config Config) *Client { go client.handleEventLoop() go client.handleSendLoop() + client.EmitNonBlocking(NewEvent("client", "create")) + return client } @@ -249,15 +254,26 @@ func (client *Client) Connect(addr string, ssl bool) (err error) { InsecureSkipVerify: client.config.SkipSSLVerification, }) if err != nil { + if !client.Destroyed() { + client.EmitNonBlocking(NewErrorEvent("connect", "Connect failed: "+err.Error())) + } return err } } else { conn, err = net.Dial("tcp", addr) if err != nil { + if !client.Destroyed() { + client.EmitNonBlocking(NewErrorEvent("connect", "Connect failed: "+err.Error())) + } return err } } + if client.Destroyed() { + _ = conn.Close() + return ErrDestroyed + } + client.EmitNonBlocking(NewEvent("client", "connect")) go func() { @@ -435,6 +451,11 @@ func (client *Client) Describef(targetName string, format string, a ...interface // wait for the event, or the client's destruction. func (client *Client) Emit(event Event) context.Context { event.ctx, event.cancel = context.WithCancel(client.ctx) + if client.Destroyed() { + event.cancel() + return event.ctx + } + client.events <- &event return event.ctx @@ -445,6 +466,10 @@ func (client *Client) Emit(event Event) context.Context { // returned context is for. func (client *Client) EmitNonBlocking(event Event) context.Context { event.ctx, event.cancel = context.WithCancel(client.ctx) + if client.Destroyed() { + event.cancel() + return event.ctx + } select { case client.events <- &event: @@ -1374,7 +1399,7 @@ func (client *Client) handleEvent(event *Event) { client.handleInTarget(channel, event) } } else { - // Try to target by mentioned channel name + // Try to target by mentioned channel name. for _, token := range strings.Fields(event.Text) { if client.isupport.IsChannel(token) { channel := client.Channel(token) diff --git a/handlers/ctcp.go b/handlers/ctcp.go new file mode 100644 index 0000000..46c7bca --- /dev/null +++ b/handlers/ctcp.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "github.com/gissleh/irc" + "strconv" + "strings" + "time" +) + +// CTCP implements the widely used CTCP commands (CLIENTINFO, VERSION, TIME, and PING), as well as the /ping command. +// It does not implement DCC. +// +// For every other CTCP command supported, you should expand the `ctcp.clientinfo.reply` client value like above. +func CTCP(event *irc.Event, client *irc.Client) { + switch event.Name() { + case "client.create": + if r, ok := client.Value("ctcp.clientinfo.reply").(string); ok { + if !strings.Contains(r, "ACTION PING TIME VERSION") { + client.SetValue("ctcp.clientinfo.reply", r+" ACTION PING TIME VERSION") + } + } else { + client.SetValue("ctcp.clientinfo.reply", "ACTION PING TIME VERSION") + } + case "ctcp.clientinfo": + { + response, ok := client.Value("ctcp.clientinfo.reply").(string) + if !ok { + response = "ACTION PING TIME VERSION" + } + + client.SendCTCP("CLIENTINFO", event.Nick, true, response) + } + case "ctcp.version": + { + version := "github.com/gissleh/irc v1.0" + if v, ok := client.Value("ctcp.version.reply").(string); ok { + version = v + } + + client.SendCTCP("VERSION", event.Nick, true, version) + } + case "ctcp.time": + { + client.SendCTCP("TIME", event.Nick, true, time.Now().Local().Format(time.RFC1123)) + } + case "ctcp.ping": + { + client.SendCTCP("PING", event.Nick, true, event.Text) + } + case "input.ping": + { + args := strings.SplitN(event.Text, " ", 2) + targetName := args[0] + if targetName == "" { + client.EmitNonBlocking(irc.NewErrorEvent("ctcp.pingarg", "/ping needs an argument")) + break + } + + client.SendCTCP("PING", targetName, false, strconv.FormatInt(time.Now().UnixNano()/1000000, 10)) + } + } +} diff --git a/handlers/mroleplay.go b/handlers/mroleplay.go index 172966c..62981ff 100644 --- a/handlers/mroleplay.go +++ b/handlers/mroleplay.go @@ -99,7 +99,7 @@ func MRoleplay(event *irc.Event, client *irc.Client) { case "input.scenec", "input.narratorc": { if event.Text == "" { - client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /scenec ")) + client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /"+event.Verb()+" ")) break }