From f7d53f4c6379bbbbe85b106ac6f3361704de3b98 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Wed, 22 Jul 2020 20:08:22 +0200 Subject: [PATCH] a few small things. --- channel.go | 4 +-- client.go | 71 ++++++++++++++++++++++++++++++++++++++++---- cmd/ircrepl/main.go | 33 ++++++++++++++------ handlers/input.go | 24 +++++++++++---- isupport/isupport.go | 4 +++ query.go | 4 +-- state.go | 27 ++++++++--------- status.go | 4 +-- target.go | 2 +- 9 files changed, 132 insertions(+), 41 deletions(-) diff --git a/channel.go b/channel.go index bb70853..9c13d12 100644 --- a/channel.go +++ b/channel.go @@ -23,8 +23,8 @@ func (channel *Channel) Name() string { return channel.name } -func (channel *Channel) State() TargetState { - return TargetState{ +func (channel *Channel) State() ClientStateTarget { + return ClientStateTarget{ Kind: "channel", Name: channel.name, Users: channel.userlist.Users(), diff --git a/client.go b/client.go index a6673c1..6a2a403 100644 --- a/client.go +++ b/client.go @@ -109,10 +109,10 @@ func New(ctx context.Context, config Config) *Client { status: &Status{}, } - _, _ = client.AddTarget(client.status) - client.ctx, client.cancel = context.WithCancel(ctx) + _, _ = client.AddTarget(client.status) + go client.handleEventLoop() go client.handleSendLoop() @@ -187,6 +187,14 @@ func (client *Client) Ready() bool { return client.ready } +// HasQuit returns true if the client had manually quit. +func (client *Client) HasQuit() bool { + client.mutex.RLock() + defer client.mutex.RUnlock() + + return client.ready +} + func (client *Client) State() ClientState { client.mutex.RLock() @@ -196,9 +204,10 @@ func (client *Client) State() ClientState { Host: client.host, Connected: client.conn != nil, Ready: client.ready, + Quit: client.quit, ISupport: client.isupport.State(), Caps: make([]string, 0, len(client.capEnabled)), - Targets: make([]TargetState, 0, len(client.targets)), + Targets: make([]ClientStateTarget, 0, len(client.targets)), } for key, enabled := range client.capEnabled { @@ -266,13 +275,21 @@ func (client *Client) Connect(addr string, ssl bool) (err error) { event, err := ParsePacket(line) if err != nil { - client.EmitNonBlocking(NewErrorEvent("parse", "Read failed: "+err.Error())) + client.mutex.RLock() + hasQuit := client.quit + client.mutex.RUnlock() + + if !hasQuit { + client.EmitNonBlocking(NewErrorEvent("parse", "Read failed: "+err.Error())) + } continue } client.EmitNonBlocking(event) } + _ = client.conn.Close() + client.mutex.Lock() client.conn = nil client.ready = false @@ -461,6 +478,16 @@ func (client *Client) EmitSync(ctx context.Context, event Event) (err error) { func (client *Client) EmitInput(line string, target Target) context.Context { event := ParseInput(line) + client.mutex.RLock() + if target != nil && client.targetIds[target] == "" { + client.EmitNonBlocking(NewErrorEvent("invalid_target", "Target does not exist.")) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return ctx + } + client.mutex.RUnlock() + if target != nil { client.mutex.RLock() event.targets = append(event.targets, target) @@ -535,13 +562,23 @@ func (client *Client) Part(channels ...string) { client.SendQueuedf("PART %s", strings.Join(channels, ",")) } +// Quit sends a quit message and marks the client as having quit, which +// means HasQuit() will return true. +func (client *Client) Quit(reason string) { + client.mutex.Lock() + client.quit = true + client.mutex.Unlock() + + client.SendQueuedf("QUIT :%s", reason) +} + // Target gets a target by kind and name func (client *Client) Target(kind string, name string) Target { client.mutex.RLock() defer client.mutex.RUnlock() for _, target := range client.targets { - if target.Kind() == kind && target.Name() == name { + if target.Kind() == kind && strings.EqualFold(name, target.Name()) { return target } } @@ -631,6 +668,12 @@ func (client *Client) AddTarget(target Target) (id string, err error) { client.targets = append(client.targets, target) client.targetIds[target] = id + event := NewEvent("hook", "add_target") + event.Args = []string{client.targetIds[target], target.Kind(), target.Name()} + event.targets = []Target{target} + event.targetIds[target] = id + client.EmitNonBlocking(event) + return } @@ -647,6 +690,10 @@ func (client *Client) RemoveTarget(target Target) (id string, err error) { if target == client.targets[i] { id = client.targetIds[target] + event := NewEvent("hook", "remove_target") + event.Args = []string{client.targetIds[target], target.Kind(), target.Name()} + client.EmitNonBlocking(event) + client.targets[i] = client.targets[len(client.targets)-1] client.targets = client.targets[:len(client.targets)-1] delete(client.targetIds, target) @@ -834,6 +881,20 @@ func (client *Client) handleEvent(event *Event) { } client.mutex.RUnlock() + // Clear connection-specific data + client.mutex.Lock() + client.nick = "" + client.user = "" + client.host = "" + client.capsRequested = client.capsRequested[:0] + for key := range client.capData { + delete(client.capData, key) + } + for key := range client.capEnabled { + delete(client.capEnabled, key) + } + client.mutex.Unlock() + // Start registration. _ = client.Sendf("NICK %s", nick) _ = client.Sendf("USER %s 8 * :%s", client.config.User, client.config.RealName) diff --git a/cmd/ircrepl/main.go b/cmd/ircrepl/main.go index b08fc4b..00fed1d 100644 --- a/cmd/ircrepl/main.go +++ b/cmd/ircrepl/main.go @@ -20,6 +20,7 @@ var flagUser = flag.String("user", "test", "The client user/ident") var flagPass = flag.String("pass", "", "The server password") var flagServer = flag.String("server", "localhost:6667", "The server to connect to") var flagSsl = flag.Bool("ssl", false, "Whether to connect securely") +var flagSkipVerify = flag.Bool("skip-verify", false, "Skip SSL verification") func main() { ctx, cancel := context.WithCancel(context.Background()) @@ -28,11 +29,12 @@ func main() { flag.Parse() client := irc.New(ctx, irc.Config{ - Nick: *flagNick, - User: *flagUser, - Alternatives: strings.Split(*flagAlts, ","), - Password: *flagPass, - Languages: []string{"no_NB", "no", "en_US", "en"}, + Nick: *flagNick, + User: *flagUser, + Alternatives: strings.Split(*flagAlts, ","), + Password: *flagPass, + Languages: []string{"no_NB", "no", "en_US", "en"}, + SkipSSLVerification: *flagSkipVerify, }) client.AddHandler(handlers.Input) @@ -41,6 +43,7 @@ func main() { err := client.Connect(*flagServer, *flagSsl) if err != nil { fmt.Fprintf(os.Stderr, "Failed to connect: %s", err) + os.Exit(1) } var target irc.Target @@ -50,18 +53,18 @@ func main() { if client.ISupport().IsChannel(name) { log.Println("Set target channel", name) - target = client.Channel(name) + target = client.Target("target", name) } else if len(name) > 0 { log.Println("Set target query", name) - target = client.Query(name) + target = client.Target("query", name) } else { log.Println("Set target status") - target = client.Status() + target = client.Target("status", "status") } if target == nil { log.Println("Target does not exist, set to status") - target = client.Status() + target = client.Target("status", "status") } event.PreventDefault() @@ -80,6 +83,18 @@ func main() { return } + if event.Name() == "hook.remove_target" { + if target != nil && target.Name() == event.Arg(2) && target.Kind() == event.Arg(1) { + log.Println("Unset target ", event.Arg(1), event.Arg(2)) + target = nil + } + } + + if event.Name() == "hook.add_target" { + log.Println("Set target ", event.Arg(1), event.Arg(2)) + target = client.Target(event.Arg(1), event.Arg(2)) + } + j, err := json.MarshalIndent(event, "", " ") if err != nil { return diff --git a/handlers/input.go b/handlers/input.go index c6510f7..a1a3436 100644 --- a/handlers/input.go +++ b/handlers/input.go @@ -122,17 +122,29 @@ func Input(event *irc.Event, client *irc.Client) { event.PreventDefault() if event.Text == "" { - client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /m ")) + client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /m ")) break } - channel := event.ChannelTarget() - if channel == nil { - client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel")) - break + if channel := event.ChannelTarget(); channel != nil { + client.SendQueuedf("MODE %s %s", channel.Name(), event.Text) + } else if status := event.StatusTarget(); status != nil { + client.SendQueuedf("MODE %s %s", client.Nick(), event.Text) + } else { + client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel or status")) + } + } + + case "input.quit", "input.disconnect": + { + event.PreventDefault() + + reason := event.Text + if reason == "" { + reason = "Client Quit" } - client.SendQueuedf("MODE %s %s", channel.Name(), event.Text) + client.Quit(reason) } } } diff --git a/isupport/isupport.go b/isupport/isupport.go index 2310021..3ed9b79 100644 --- a/isupport/isupport.go +++ b/isupport/isupport.go @@ -199,6 +199,10 @@ func (isupport *ISupport) Prefixes(modes string) string { // IsChannel returns whether the target name is a channel. func (isupport *ISupport) IsChannel(targetName string) bool { + if len(targetName) < 1 { + return false + } + isupport.lock.RLock() defer isupport.lock.RUnlock() diff --git a/query.go b/query.go index fb9361b..34fccf5 100644 --- a/query.go +++ b/query.go @@ -19,8 +19,8 @@ func (query *Query) Name() string { return query.user.Nick } -func (query *Query) State() TargetState { - return TargetState{ +func (query *Query) State() ClientStateTarget { + return ClientStateTarget{ Kind: "query", Name: query.user.Nick, Users: []list.User{query.user}, diff --git a/state.go b/state.go index 45b6c58..8be756f 100644 --- a/state.go +++ b/state.go @@ -5,25 +5,24 @@ import ( "github.com/gissleh/irc/list" ) +// ClientState is a serializable snapshot of the client's state. type ClientState struct { - ID string `json:"id"` - Nick string `json:"nick"` - User string `json:"user"` - Host string `json:"host"` - Connected bool `json:"connected"` - Ready bool `json:"quit"` - ISupport *isupport.State `json:"isupport"` - Caps []string `json:"caps"` - Targets []TargetState `json:"targets"` + ID string `json:"id"` + Nick string `json:"nick"` + User string `json:"user"` + Host string `json:"host"` + Connected bool `json:"connected"` + Ready bool `json:"ready"` + Quit bool `json:"quit"` + ISupport *isupport.State `json:"isupport"` + Caps []string `json:"caps"` + Targets []ClientStateTarget `json:"targets"` } -type TargetState struct { +// ClientStateTarget is a part of the ClientState representing a target's state at the time of snapshot. +type ClientStateTarget struct { ID string `json:"id"` Kind string `json:"kind"` Name string `json:"name"` Users []list.User `json:"users,omitempty"` } - -type EventData struct { - -} \ No newline at end of file diff --git a/status.go b/status.go index 30c6f34..a0b03f3 100644 --- a/status.go +++ b/status.go @@ -14,8 +14,8 @@ func (status *Status) Name() string { return "Status" } -func (status *Status) State() TargetState { - return TargetState{ +func (status *Status) State() ClientStateTarget { + return ClientStateTarget{ Kind: "status", Name: "Status", Users: nil, diff --git a/target.go b/target.go index 0405c44..e445ec2 100644 --- a/target.go +++ b/target.go @@ -6,5 +6,5 @@ type Target interface { Kind() string Name() string Handle(event *Event, client *Client) - State() TargetState + State() ClientStateTarget }