diff options
Diffstat (limited to 'teleirc/matterbridge/vendor/github.com/lrstanley')
19 files changed, 6662 insertions, 0 deletions
diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/.editorconfig b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/.editorconfig new file mode 100644 index 0000000..32ecf3e --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/.editorconfig @@ -0,0 +1,50 @@ +# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. +# +# editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 100 + +[*.tf] +indent_size = 2 + +[*.go] +indent_style = tab +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.{md,py,sh,yml,yaml,cjs,js,ts,vue,css}] +max_line_length = 105 + +[*.{yml,yaml,toml}] +indent_size = 2 + +[*.json] +indent_size = 2 +insert_final_newline = ignore + +[*.html] +max_line_length = 140 +indent_size = 2 + +[*.{cjs,js,ts,vue,css}] +indent_size = 2 + +[Makefile] +indent_style = tab + +[**.min.js] +indent_style = ignore +insert_final_newline = ignore + +[*.bat] +indent_style = tab diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/.golangci.yml b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/.golangci.yml new file mode 100644 index 0000000..1a8320c --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/.golangci.yml @@ -0,0 +1,38 @@ +# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. +run: + tests: False + timeout: 3m + +issues: + max-per-linter: 0 + max-same-issues: 0 + +severity: + default-severity: error + rules: + - linters: + - errcheck + - gocritic + severity: warning + +linters: + enable: + - asciicheck + - exportloopref + - gci + - gocritic + - gofmt + - misspell + +linters-settings: + gocritic: + disabled-checks: + - hugeParam + - ifElseChain + enabled-tags: + - diagnostic + - opinionated + - performance + - style + govet: + check-shadowing: true diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/LICENSE b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/LICENSE new file mode 100644 index 0000000..073d8c0 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Liam Stanley <me@liamstanley.io> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/README.md b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/README.md new file mode 100644 index 0000000..ddb0bc1 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/README.md @@ -0,0 +1,172 @@ +<p align="center"><a href="https://pkg.go.dev/github.com/lrstanley/girc"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p> +<!-- template:begin:header --> +<!-- do not edit anything in this "template" block, its auto-generated --> + +<p align="center">girc -- :bomb: girc is a flexible IRC library for Go :ok_hand:</p> +<p align="center"> + <a href="https://github.com/lrstanley/girc/tags"> + <img title="Latest Semver Tag" src="https://img.shields.io/github/v/tag/lrstanley/girc?style=flat-square"> + </a> + <a href="https://github.com/lrstanley/girc/commits/master"> + <img title="Last commit" src="https://img.shields.io/github/last-commit/lrstanley/girc?style=flat-square"> + </a> + + <a href="https://github.com/lrstanley/girc/actions?query=workflow%3Atest+event%3Apush"> + <img title="GitHub Workflow Status (test @ master)" src="https://img.shields.io/github/actions/workflow/status/lrstanley/girc/test.yml?branch=master&label=test&style=flat-square"> + </a> + + <a href="https://codecov.io/gh/lrstanley/girc"> + <img title="Code Coverage" src="https://img.shields.io/codecov/c/github/lrstanley/girc/master?style=flat-square"> + </a> + + <a href="https://pkg.go.dev/github.com/lrstanley/girc"> + <img title="Go Documentation" src="https://pkg.go.dev/badge/github.com/lrstanley/girc?style=flat-square"> + </a> + <a href="https://goreportcard.com/report/github.com/lrstanley/girc"> + <img title="Go Report Card" src="https://goreportcard.com/badge/github.com/lrstanley/girc?style=flat-square"> + </a> +</p> +<p align="center"> + <a href="https://github.com/lrstanley/girc/issues?q=is:open+is:issue+label:bug"> + <img title="Bug reports" src="https://img.shields.io/github/issues/lrstanley/girc/bug?label=issues&style=flat-square"> + </a> + <a href="https://github.com/lrstanley/girc/issues?q=is:open+is:issue+label:enhancement"> + <img title="Feature requests" src="https://img.shields.io/github/issues/lrstanley/girc/enhancement?label=feature%20requests&style=flat-square"> + </a> + <a href="https://github.com/lrstanley/girc/pulls"> + <img title="Open Pull Requests" src="https://img.shields.io/github/issues-pr/lrstanley/girc?label=prs&style=flat-square"> + </a> + <a href="https://github.com/lrstanley/girc/discussions/new?category=q-a"> + <img title="Ask a Question" src="https://img.shields.io/badge/support-ask_a_question!-blue?style=flat-square"> + </a> + <a href="https://liam.sh/chat"><img src="https://img.shields.io/badge/discord-bytecord-blue.svg?style=flat-square" title="Discord Chat"></a> +</p> +<!-- template:end:header --> + +<!-- template:begin:toc --> +<!-- do not edit anything in this "template" block, its auto-generated --> +## :link: Table of Contents + + - [Features](#features) + - [Installing](#installing) + - [Examples](#examples) + - [References](#references) + - [Support & Assistance](#raising_hand_man-support--assistance) + - [Contributing](#handshake-contributing) + - [License](#balance_scale-license) +<!-- template:end:toc --> + +## Features + +- Focuses on simplicity, yet tries to still be flexible. +- Only requires [standard library packages](https://godoc.org/github.com/lrstanley/girc?imports) +- Event based triggering/responses ([example](https://godoc.org/github.com/lrstanley/girc#ex-package--Commands), and [CTCP too](https://godoc.org/github.com/lrstanley/girc#Commands.SendCTCP)!) +- [Documentation](https://godoc.org/github.com/lrstanley/girc) is _mostly_ complete. +- Support for a good portion of the [IRCv3 spec](http://ircv3.net/software/libraries.html). + - SASL Auth (currently only `PLAIN` and `EXTERNAL` is support by default, + however you can simply implement `SASLMech` yourself to support additional + mechanisms.) + - Message tags (things like `account-tag` on by default) + - `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://github.com/lrstanley/girc/blob/master/cap.go) for more info). +- Channel and user tracking. Easily find what users are in a channel, if a + user is away, or if they are authenticated (if the server supports it!) +- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/github.com/lrstanley/girc#Client.LookupChannel), [LookupUser](https://godoc.org/github.com/lrstanley/girc#Client.LookupUser), [GetServerOption (ISUPPORT)](https://godoc.org/github.com/lrstanley/girc#Client.GetServerOption), etc.) +- Built-in support for things you would commonly have to implement yourself. + - Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/github.com/lrstanley/girc#Config).) + - Event/message rate limiting. + - Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/github.com/lrstanley/girc#IsValidChannel), [IsValidNick](https://godoc.org/github.com/lrstanley/girc#IsValidNick), etc.) + - CTCP handling and auto-responses ([CTCP](https://godoc.org/github.com/lrstanley/girc#CTCP)) + - And more! + +## Installing + + $ go get -u github.com/lrstanley/girc + +## Examples + +See [the examples](https://godoc.org/github.com/lrstanley/girc#example-package--Bare) +within the documentation for real-world usecases. Here are a few real-world +usecases/examples/projects which utilize girc: + +| Project | Description | +| --- | --- | +| [nagios-check-ircd](https://github.com/lrstanley/nagios-check-ircd) | Nagios utility for monitoring the health of an ircd | +| [nagios-notify-irc](https://github.com/lrstanley/nagios-notify-irc) | Nagios utility for sending alerts to one or many channels/networks | +| [matterbridge](https://github.com/42wim/matterbridge) | bridge between mattermost, IRC, slack, discord (and many others) with REST API | + +Working on a project and want to add it to the list? Submit a pull request! + +## References + + * [IRCv3: Specification Docs](http://ircv3.net/irc/) + * [IRCv3: Specification Repo](https://github.com/ircv3/ircv3-specifications) + * [IRCv3 Capability Registry](http://ircv3.net/registry.html) + * [IRCv3: WEBIRC](https://ircv3.net/specs/extensions/webirc.html) + * [KiwiIRC: WEBIRC](https://kiwiirc.com/docs/webirc) + * [ISUPPORT Specification Docs](http://www.irc.org/tech_docs/005.html) ([alternative 1](http://defs.ircdocs.horse/defs/isupport.html), [alternative 2](https://github.com/grawity/irc-docs/blob/master/client/RPL_ISUPPORT/draft-hardy-irc-isupport-00.txt), [relevant draft](http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt)) + * [IRC Numerics List](http://defs.ircdocs.horse/defs/numerics.html) + * [Extended WHO (also known as WHOX)](https://github.com/quakenet/snircd/blob/master/doc/readme.who) + * [RFC1459: Internet Relay Chat Protocol](https://tools.ietf.org/html/rfc1459) + * [RFC2812: Internet Relay Chat: Client Protocol](https://tools.ietf.org/html/rfc2812) + * [RFC2813: Internet Relay Chat: Server Protocol](https://tools.ietf.org/html/rfc2813) + * [RFC7194: Default Port for Internet Relay Chat (IRC) via TLS/SSL](https://tools.ietf.org/html/rfc7194) + * [RFC4422: Simple Authentication and Security Layer](https://tools.ietf.org/html/rfc4422) ([SASL EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)) + * [RFC4616: The PLAIN SASL Mechanism](https://tools.ietf.org/html/rfc4616) + + +<!-- template:begin:support --> +<!-- do not edit anything in this "template" block, its auto-generated --> +## :raising_hand_man: Support & Assistance + +* :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for + guidelines on ensuring everyone has the best experience interacting with + the community. +* :raising_hand_man: Take a look at the [support](.github/SUPPORT.md) document on + guidelines for tips on how to ask the right questions. +* :lady_beetle: For all features/bugs/issues/questions/etc, [head over here](https://github.com/lrstanley/girc/issues/new/choose). +<!-- template:end:support --> + +<!-- template:begin:contributing --> +<!-- do not edit anything in this "template" block, its auto-generated --> +## :handshake: Contributing + +* :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for guidelines + on ensuring everyone has the best experience interacting with the + community. +* :clipboard: Please review the [contributing](.github/CONTRIBUTING.md) doc for submitting + issues/a guide on submitting pull requests and helping out. +* :old_key: For anything security related, please review this repositories [security policy](https://github.com/lrstanley/girc/security/policy). +<!-- template:end:contributing --> + +<!-- template:begin:license --> +<!-- do not edit anything in this "template" block, its auto-generated --> +## :balance_scale: License + +``` +MIT License + +Copyright (c) 2016 Liam Stanley <me@liamstanley.io> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +_Also located [here](LICENSE)_ +<!-- template:end:license --> +girc artwork licensed under [CC 3.0](http://creativecommons.org/licenses/by/3.0/) +based on Renee French under Creative Commons 3.0 Attributions. diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/builtin.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/builtin.go new file mode 100644 index 0000000..e60c577 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/builtin.go @@ -0,0 +1,518 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "strings" + "time" +) + +// registerBuiltin sets up built-in handlers, based on client +// configuration. +func (c *Client) registerBuiltins() { + c.debug.Print("registering built-in handlers") + c.Handlers.mu.Lock() + + // Built-in things that should always be supported. + c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect)) + c.Handlers.register(true, false, PING, HandlerFunc(handlePING)) + c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG)) + + if !c.Config.disableTracking { + // Joins/parts/anything that may add/remove/rename users. + c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN)) + c.Handlers.register(true, false, PART, HandlerFunc(handlePART)) + c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK)) + c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT)) + c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK)) + c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES)) + + // Modes. + c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE)) + c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE)) + + // WHO/WHOX responses. + c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO)) + c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO)) + + // Other misc. useful stuff. + c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC)) + c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC)) + c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO)) + c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT)) + c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD)) + c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD)) + + // Keep users lastactive times up to date. + c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive)) + c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive)) + c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive)) + c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive)) + + // CAP IRCv3-specific tracking and functionality. + c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP)) + c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST)) + c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY)) + c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT)) + c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags)) + + // SASL IRCv3 support. + c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL)) + c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL)) + c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError)) + c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError)) + c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError)) + c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError)) + c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError)) + } + + // Nickname collisions. + c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler)) + c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler)) + c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler)) + + c.Handlers.mu.Unlock() +} + +// handleConnect is a helper function which lets the client know that enough +// time has passed and now they can send commands. +// +// Should always run in separate thread due to blocking delay. +func handleConnect(c *Client, e Event) { + // This should be the nick that the server gives us. 99% of the time, it's + // the one we supplied during connection, but some networks will rename + // users on connect. + if len(e.Params) > 0 { + c.state.Lock() + c.state.nick = e.Params[0] + c.state.Unlock() + + c.state.notify(c, UPDATE_GENERAL) + } + + time.Sleep(2 * time.Second) + + c.mu.RLock() + server := c.server() + c.mu.RUnlock() + c.RunHandlers(&Event{Command: CONNECTED, Params: []string{server}}) +} + +// nickCollisionHandler helps prevent the client from having conflicting +// nicknames with another bot, user, etc. +func nickCollisionHandler(c *Client, e Event) { + if c.Config.HandleNickCollide == nil { + c.Cmd.Nick(c.GetNick() + "_") + return + } + + newNick := c.Config.HandleNickCollide(c.GetNick()) + if newNick != "" { + c.Cmd.Nick(newNick) + } +} + +// handlePING helps respond to ping requests from the server. +func handlePING(c *Client, e Event) { + c.Cmd.Pong(e.Last()) +} + +func handlePONG(c *Client, e Event) { + c.conn.mu.Lock() + c.conn.lastPong = time.Now() + c.conn.mu.Unlock() +} + +// handleJOIN ensures that the state has updated users and channels. +func handleJOIN(c *Client, e Event) { + if e.Source == nil || len(e.Params) == 0 { + return + } + + channelName := e.Params[0] + + c.state.Lock() + + channel := c.state.lookupChannel(channelName) + if channel == nil { + if ok := c.state.createChannel(channelName); !ok { + c.state.Unlock() + return + } + + channel = c.state.lookupChannel(channelName) + } + + user := c.state.lookupUser(e.Source.Name) + if user == nil { + if ok := c.state.createUser(e.Source); !ok { + c.state.Unlock() + return + } + user = c.state.lookupUser(e.Source.Name) + } + + defer c.state.notify(c, UPDATE_STATE) + + channel.addUser(user.Nick) + user.addChannel(channel.Name) + + // Assume extended-join (ircv3). + if len(e.Params) >= 2 { + if e.Params[1] != "*" { + user.Extras.Account = e.Params[1] + } + + if len(e.Params) > 2 { + user.Extras.Name = e.Params[2] + } + } + c.state.Unlock() + + if e.Source.ID() == c.GetID() { + // If it's us, don't just add our user to the list. Run a WHO which + // will tell us who exactly is in the entire channel. + c.Send(&Event{Command: WHO, Params: []string{channelName, "%tacuhnr,1"}}) + + // Also send a MODE to obtain the list of channel modes. + c.Send(&Event{Command: MODE, Params: []string{channelName}}) + + // Update our ident and host too, in state -- since there is no + // cleaner method to do this. + c.state.Lock() + c.state.ident = e.Source.Ident + c.state.host = e.Source.Host + c.state.Unlock() + return + } + + // Only WHO the user, which is more efficient. + c.Send(&Event{Command: WHO, Params: []string{e.Source.Name, "%tacuhnr,1"}}) +} + +// handlePART ensures that the state is clean of old user and channel entries. +func handlePART(c *Client, e Event) { + if e.Source == nil || len(e.Params) < 1 { + return + } + + // TODO: does this work if it's not the bot? + + channel := e.Params[0] + + if channel == "" { + return + } + + defer c.state.notify(c, UPDATE_STATE) + + if e.Source.ID() == c.GetID() { + c.state.Lock() + c.state.deleteChannel(channel) + c.state.Unlock() + return + } + + c.state.Lock() + c.state.deleteUser(channel, e.Source.ID()) + c.state.Unlock() +} + +// handleTOPIC handles incoming TOPIC events and keeps channel tracking info +// updated with the latest channel topic. +func handleTOPIC(c *Client, e Event) { + var name string + switch len(e.Params) { + case 0: + return + case 1: + name = e.Params[0] + default: + name = e.Params[1] + } + + c.state.Lock() + channel := c.state.lookupChannel(name) + if channel == nil { + c.state.Unlock() + return + } + + channel.Topic = e.Last() + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handlWHO updates our internal tracking of users/channels with WHO/WHOX +// information. +func handleWHO(c *Client, e Event) { + var ident, host, nick, account, realname string + + // Assume WHOX related. + if e.Command == RPL_WHOSPCRPL { + if len(e.Params) != 8 { + // Assume there was some form of error or invalid WHOX response. + return + } + + if e.Params[1] != "1" { + // We should always be sending 1, and we should receive 1. If this + // is anything but, then we didn't send the request and we can + // ignore it. + return + } + + ident, host, nick, account = e.Params[3], e.Params[4], e.Params[5], e.Params[6] + realname = e.Last() + } else { + // Assume RPL_WHOREPLY. + // format: "<client> <channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real_name>" + ident, host, nick, realname = e.Params[2], e.Params[3], e.Params[5], e.Last() + + // Strip the numbers from "<hopcount> <realname>" + for i := 0; i < len(realname); i++ { + // Check if it's not 0-9. + if realname[i] < 0x30 || i > 0x39 { + realname = strings.TrimLeft(realname[i+1:], " ") + break + } + + if i == len(realname)-1 { + // Assume it's only numbers? + realname = "" + } + } + } + + c.state.Lock() + user := c.state.lookupUser(nick) + if user == nil { + c.state.Unlock() + return + } + + user.Host = host + user.Ident = ident + user.Extras.Name = realname + + if account != "0" { + user.Extras.Account = account + } + + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleKICK ensures that users are cleaned up after being kicked from the +// channel +func handleKICK(c *Client, e Event) { + if len(e.Params) < 2 { + // Needs at least channel and user. + return + } + + defer c.state.notify(c, UPDATE_STATE) + + if e.Params[1] == c.GetNick() { + c.state.Lock() + c.state.deleteChannel(e.Params[0]) + c.state.Unlock() + return + } + + // Assume it's just another user. + c.state.Lock() + c.state.deleteUser(e.Params[0], e.Params[1]) + c.state.Unlock() +} + +// handleNICK ensures that users are renamed in state, or the client name is +// up to date. +func handleNICK(c *Client, e Event) { + if e.Source == nil { + return + } + + c.state.Lock() + // renameUser updates the LastActive time automatically. + if len(e.Params) >= 1 { + c.state.renameUser(e.Source.ID(), e.Last()) + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleQUIT handles users that are quitting from the network. +func handleQUIT(c *Client, e Event) { + if e.Source == nil { + return + } + + if e.Source.ID() == c.GetID() { + return + } + + c.state.Lock() + c.state.deleteUser("", e.Source.ID()) + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleMYINFO handles incoming MYINFO events -- these are commonly used +// to tell us what the server name is, what version of software is being used +// as well as what channel and user modes are being used on the server. +func handleMYINFO(c *Client, e Event) { + // Malformed or odd output. As this can differ strongly between networks, + // just skip it. + if len(e.Params) < 3 { + return + } + + c.state.Lock() + c.state.serverOptions["SERVER"] = e.Params[1] + c.state.serverOptions["VERSION"] = e.Params[2] + c.state.Unlock() + c.state.notify(c, UPDATE_GENERAL) +} + +// handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL) +// events. These commonly contain the server capabilities and limitations. +// For example, things like max channel name length, or nickname length. +func handleISUPPORT(c *Client, e Event) { + // Must be a ISUPPORT-based message. + + // Also known as RPL_PROTOCTL. + if !strings.HasSuffix(e.Last(), "this server") { + return + } + + // Must have at least one configuration. + if len(e.Params) < 2 { + return + } + + c.state.Lock() + // Skip the first parameter, as it's our nickname, and the last, as it's the doc. + for i := 1; i < len(e.Params)-1; i++ { + j := strings.IndexByte(e.Params[i], '=') + + if j < 1 || (j+1) == len(e.Params[i]) { + c.state.serverOptions[e.Params[i]] = "" + continue + } + + name := e.Params[i][0:j] + val := e.Params[i][j+1:] + c.state.serverOptions[name] = val + } + c.state.Unlock() + c.state.notify(c, UPDATE_GENERAL) +} + +// handleMOTD handles incoming MOTD messages and buffers them up for use with +// Client.ServerMOTD(). +func handleMOTD(c *Client, e Event) { + c.state.Lock() + + defer c.state.notify(c, UPDATE_GENERAL) + + // Beginning of the MOTD. + if e.Command == RPL_MOTDSTART { + c.state.motd = "" + + c.state.Unlock() + return + } + + // Otherwise, assume we're getting sent the MOTD line-by-line. + if c.state.motd != "" { + c.state.motd += "\n" + } + c.state.motd += e.Last() + c.state.Unlock() +} + +// handleNAMES handles incoming NAMES queries, of which lists all users in +// a given channel. Optionally also obtains ident/host values, as well as +// permissions for each user, depending on what capabilities are enabled. +func handleNAMES(c *Client, e Event) { + if len(e.Params) < 1 { + return + } + + channel := c.state.lookupChannel(e.Params[2]) + if channel == nil { + return + } + + parts := strings.Split(e.Last(), " ") + + var modes, nick string + var ok bool + var s *Source + + c.state.Lock() + for i := 0; i < len(parts); i++ { + modes, nick, ok = parseUserPrefix(parts[i]) + if !ok { + continue + } + + // If userhost-in-names. + if strings.Contains(nick, "@") { + s = ParseSource(nick) + if s == nil { + continue + } + + } else { + s = &Source{ + Name: nick, + } + + if !IsValidNick(s.Name) { + continue + } + } + + c.state.createUser(s) + user := c.state.lookupUser(s.Name) + if user == nil { + continue + } + + user.addChannel(channel.Name) + channel.addUser(s.ID()) + + // Don't append modes, overwrite them. + perms, _ := user.Perms.Lookup(channel.Name) + perms.set(modes, false) + user.Perms.set(channel.Name, perms) + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// updateLastActive is a wrapper for any event which the source author +// should have it's LastActive time updated. This is useful for things like +// a KICK where we know they are active, as they just kicked another user, +// even though they may not be talking. +func updateLastActive(c *Client, e Event) { + if e.Source == nil { + return + } + + c.state.Lock() + + // Update the users last active time, if they exist. + user := c.state.lookupUser(e.Source.Name) + if user == nil { + c.state.Unlock() + return + } + + user.LastActive = time.Now() + c.state.Unlock() +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap.go new file mode 100644 index 0000000..631b925 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap.go @@ -0,0 +1,355 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// Something not in the list? Depending on the type of capability, you can +// enable it using Config.SupportedCaps. +var possibleCap = map[string][]string{ + "account-notify": nil, + "account-tag": nil, + "away-notify": nil, + "batch": nil, + "cap-notify": nil, + "chghost": nil, + "extended-join": nil, + "invite-notify": nil, + "message-tags": nil, + "msgid": nil, + "multi-prefix": nil, + "server-time": nil, + "userhost-in-names": nil, + + // Supported draft versions, some may be duplicated above, this is for backwards + // compatibility. + "draft/message-tags-0.2": nil, + "draft/msgid": nil, + + // sts, sasl, etc are enabled dynamically/depending on client configuration, + // so aren't included on this list. + + // "echo-message" is supported, but it's not enabled by default. This is + // to prevent unwanted confusion and utilize less traffic if it's not needed. + // echo messages aren't sent to girc.PRIVMSG and girc.NOTICE handlers, + // rather they are only sent to girc.ALL_EVENTS handlers (this is to prevent + // each handler to have to check these types of things for each message). + // You can compare events using Event.Equals() to see if they are the same. +} + +// https://ircv3.net/specs/extensions/server-time-3.2.html +// <value> ::= YYYY-MM-DDThh:mm:ss.sssZ +const capServerTimeFormat = "2006-01-02T15:04:05.999Z" + +func (c *Client) listCAP() { + if !c.Config.disableTracking { + c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}}) + } +} + +func possibleCapList(c *Client) map[string][]string { + out := make(map[string][]string) + + if c.Config.SASL != nil { + out["sasl"] = nil + } + + if !c.Config.DisableSTS && !c.Config.SSL { + // If fallback supported, and we failed recently, don't try negotiating STS. + // ONLY do this fallback if we're expired (primarily useful during the first + // sts negotiation). + if time.Since(c.state.sts.lastFailed) < 5*time.Minute && !c.Config.DisableSTSFallback { + c.debug.Println("skipping strict transport policy negotiation; failed within the last 5 minutes") + } else { + out["sts"] = nil + } + } + + for k := range c.Config.SupportedCaps { + out[k] = c.Config.SupportedCaps[k] + } + + for k := range possibleCap { + out[k] = possibleCap[k] + } + + return out +} + +func parseCap(raw string) map[string]map[string]string { + out := make(map[string]map[string]string) + parts := strings.Split(raw, " ") + + var val int + + for i := 0; i < len(parts); i++ { + val = strings.IndexByte(parts[i], prefixTagValue) // = + + // No value splitter, or has splitter but no trailing value. + if val < 1 || len(parts[i]) < val+1 { + // The capability doesn't contain a value. + out[parts[i]] = nil + continue + } + + out[parts[i][:val]] = make(map[string]string) + for _, option := range strings.Split(parts[i][val+1:], ",") { + j := strings.Index(option, "=") + + if j < 0 { + out[parts[i][:val]][option] = "" + } else { + out[parts[i][:val]][option[:j]] = option[j+1:] + } + } + } + + return out +} + +// handleCAP attempts to find out what IRCv3 capabilities the server supports. +// This will lock further registration until we have acknowledged (or denied) +// the capabilities. +func handleCAP(c *Client, e Event) { + c.state.Lock() + defer c.state.Unlock() + + if len(e.Params) >= 2 && e.Params[1] == CAP_DEL { + caps := parseCap(e.Last()) + for cap := range caps { + // TODO: test the deletion. + delete(c.state.enabledCap, cap) + } + return + } + + // We can assume there was a failure attempting to enable a capability. + if len(e.Params) >= 2 && e.Params[1] == CAP_NAK { + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + possible := possibleCapList(c) + // TODO: test the addition. + if len(e.Params) >= 3 && (e.Params[1] == CAP_LS || e.Params[1] == CAP_NEW) { + caps := parseCap(e.Last()) + + for capName := range caps { + if _, ok := possible[capName]; !ok { + continue + } + + if len(possible[capName]) == 0 || len(caps[capName]) == 0 { + c.state.tmpCap[capName] = caps[capName] + continue + } + + var contains bool + + for capAttr := range caps[capName] { + for i := 0; i < len(possible[capName]); i++ { + if _, ok := caps[capName][capAttr]; ok { + // Assuming we have a matching attribute for the capability. + contains = true + goto checkcontains + } + } + } + + checkcontains: + if !contains { + continue + } + + c.state.tmpCap[capName] = caps[capName] + } + + // Indicates if this is a multi-line LS. (3 args means it's the + // last LS). + if len(e.Params) == 3 { + // If we support no caps, just ack the CAP message and END. + if len(c.state.tmpCap) == 0 { + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + // Let them know which ones we'd like to enable. + reqKeys := make([]string, len(c.state.tmpCap)) + i := 0 + for k := range c.state.tmpCap { + reqKeys[i] = k + i++ + } + c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(reqKeys, " ")}}) + } + } + + if len(e.Params) == 3 && e.Params[1] == CAP_ACK { + enabled := strings.Split(e.Last(), " ") + for _, cap := range enabled { + if val, ok := c.state.tmpCap[cap]; ok { + c.state.enabledCap[cap] = val + } else { + c.state.enabledCap[cap] = nil + } + } + + // Anything client side that needs to be setup post-capability-acknowledgement, + // should be done here. + + // Handle STS, and only if it's something specifically we enabled (client + // may choose to disable girc automatic STS, and do it themselves). + if sts, sok := c.state.enabledCap["sts"]; sok && !c.Config.DisableSTS { + var isError bool + + // Some things are updated in the policy depending on if the current + // connection is over tls or not. + var hasTLSConnection bool + if tlsState, _ := c.TLSConnectionState(); tlsState != nil { + hasTLSConnection = true + } + + // "This key indicates the port number for making a secure connection. + // This key’s value MUST be a single port number. If the client is not + // already connected securely to the server at the requested hostname, + // it MUST close the insecure connection and reconnect securely on the + // stated port. + // + // To enforce an STS upgrade policy, servers MUST send this key to + // insecurely connected clients. Servers MAY send this key to securely + // connected clients, but it will be ignored." + // + // See: https://ircv3.net/specs/extensions/sts#the-port-key + if !hasTLSConnection { + if port, ok := sts["port"]; ok { + c.state.sts.upgradePort, _ = strconv.Atoi(port) + if c.state.sts.upgradePort < 21 { + isError = true + } + } else { + isError = true + } + } + + // "This key is used on secure connections to indicate how long clients + // MUST continue to use secure connections when connecting to the server + // at the requested hostname. The value of this key MUST be given as a + // single integer which represents the number of seconds until the persistence + // policy expires. + // + // To enforce an STS persistence policy, servers MUST send this key to + // securely connected clients. Servers MAY send this key to all clients, + // but insecurely connected clients MUST ignore it." + // + // See: https://ircv3.net/specs/extensions/sts#the-duration-key + if hasTLSConnection { + if duration, ok := sts["duration"]; ok { + c.state.sts.persistenceDuration, _ = strconv.Atoi(duration) + c.state.sts.persistenceReceived = time.Now() + } else { + isError = true + } + } + + // See: https://ircv3.net/specs/extensions/sts#the-preload-key + if hasTLSConnection { + if preload, ok := sts["preload"]; ok { + c.state.sts.preload, _ = strconv.ParseBool(preload) + } + } + + if isError { + c.rx <- &Event{Command: ERROR, Params: []string{ + fmt.Sprintf("closing connection: strict transport policy provided by server is invalid; possible MITM? config: %#v", sts), + }} + return + } + + // Only upgrade if not already upgraded. + if !hasTLSConnection { + c.state.sts.beginUpgrade = true + + c.RunHandlers(&Event{Command: STS_UPGRADE_INIT}) + c.debug.Println("strict transport security policy provided by server; closing connection to begin upgrade...") + c.Close() + return + } + } + + // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests + // due to cap-notify, we can re-evaluate what we can support. + c.state.tmpCap = make(map[string]map[string]string) + + if _, ok := c.state.enabledCap["sasl"]; ok && c.Config.SASL != nil { + c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}}) + // Don't "CAP END", since we want to authenticate. + return + } + + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } +} + +// handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is +// what occurs (when enabled) when a servers services change the hostname of +// a user. Traditionally, this was simply resolved with a quick QUIT and JOIN, +// however CHGHOST resolves this in a much cleaner fashion. +func handleCHGHOST(c *Client, e Event) { + if len(e.Params) != 2 { + return + } + + c.state.Lock() + user := c.state.lookupUser(e.Source.Name) + if user != nil { + user.Ident = e.Params[0] + user.Host = e.Params[1] + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both +// when users are no longer away, or when they are away. +func handleAWAY(c *Client, e Event) { + c.state.Lock() + user := c.state.lookupUser(e.Source.Name) + if user != nil { + user.Extras.Away = e.Last() + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when +// a user logs into an account, logs out of their account, or logs into a +// different account. The account backend is handled server-side, so this +// could be NickServ, X (undernet?), etc. +func handleACCOUNT(c *Client, e Event) { + if len(e.Params) != 1 { + return + } + + account := e.Params[0] + if account == "*" { + account = "" + } + + c.state.Lock() + user := c.state.lookupUser(e.Source.Name) + if user != nil { + user.Extras.Account = account + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap_sasl.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap_sasl.go new file mode 100644 index 0000000..d880316 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap_sasl.go @@ -0,0 +1,135 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "encoding/base64" + "fmt" +) + +// SASLMech is an representation of what a SASL mechanism should support. +// See SASLExternal and SASLPlain for implementations of this. +type SASLMech interface { + // Method returns the uppercase version of the SASL mechanism name. + Method() string + // Encode returns the response that the SASL mechanism wants to use. If + // the returned string is empty (e.g. the mechanism gives up), the handler + // will attempt to panic, as expectation is that if SASL authentication + // fails, the client will disconnect. + Encode(params []string) (output string) +} + +// SASLExternal implements the "EXTERNAL" SASL type. +type SASLExternal struct { + // Identity is an optional field which allows the client to specify + // pre-authentication identification. This means that EXTERNAL will + // supply this in the initial response. This usually isn't needed (e.g. + // CertFP). + Identity string `json:"identity"` +} + +// Method identifies what type of SASL this implements. +func (sasl *SASLExternal) Method() string { + return "EXTERNAL" +} + +// Encode for external SALS authentication should really only return a "+", +// unless the user has specified pre-authentication or identification data. +// See https://tools.ietf.org/html/rfc4422#appendix-A for more info. +func (sasl *SASLExternal) Encode(params []string) string { + if len(params) != 1 || params[0] != "+" { + return "" + } + + if sasl.Identity != "" { + return sasl.Identity + } + + return "+" +} + +// SASLPlain contains the user and password needed for PLAIN SASL authentication. +type SASLPlain struct { + User string `json:"user"` // User is the username for SASL. + Pass string `json:"pass"` // Pass is the password for SASL. +} + +// Method identifies what type of SASL this implements. +func (sasl *SASLPlain) Method() string { + return "PLAIN" +} + +// Encode encodes the plain user+password into a SASL PLAIN implementation. +// See https://tools.ietf.org/rfc/rfc4422.txt for more info. +func (sasl *SASLPlain) Encode(params []string) string { + if len(params) != 1 || params[0] != "+" { + return "" + } + + in := []byte(sasl.User) + + in = append(in, 0x0) + in = append(in, []byte(sasl.User)...) + in = append(in, 0x0) + in = append(in, []byte(sasl.Pass)...) + + return base64.StdEncoding.EncodeToString(in) +} + +const saslChunkSize = 400 + +func handleSASL(c *Client, e Event) { + if e.Command == RPL_SASLSUCCESS || e.Command == ERR_SASLALREADY { + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + // Assume they want us to handle sending auth. + auth := c.Config.SASL.Encode(e.Params) + + if auth == "" { + // Assume the SASL authentication method doesn't want to respond for + // some reason. The SASL spec and IRCv3 spec do not define a clear + // way to abort a SASL exchange, other than to disconnect, or proceed + // with CAP END. + c.rx <- &Event{Command: ERROR, Params: []string{ + fmt.Sprintf("closing connection: SASL %s failed: %s", c.Config.SASL.Method(), e.Last()), + }} + return + } + + // Send in "saslChunkSize"-length byte chunks. If the last chuck is + // exactly "saslChunkSize" bytes, send a "AUTHENTICATE +" 0-byte + // acknowledgement response to let the server know that we're done. + for { + if len(auth) > saslChunkSize { + c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[0 : saslChunkSize-1]}, Sensitive: true}) + auth = auth[saslChunkSize:] + continue + } + + if len(auth) <= saslChunkSize { + c.write(&Event{Command: AUTHENTICATE, Params: []string{auth}, Sensitive: true}) + + if len(auth) == 400 { + c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}}) + } + break + } + } +} + +func handleSASLError(c *Client, e Event) { + if c.Config.SASL == nil { + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + // Authentication failed. The SASL spec and IRCv3 spec do not define a + // clear way to abort a SASL exchange, other than to disconnect, or + // proceed with CAP END. + c.rx <- &Event{Command: ERROR, Params: []string{"closing connection: " + e.Last()}} +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap_tags.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap_tags.go new file mode 100644 index 0000000..42599f3 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/cap_tags.go @@ -0,0 +1,321 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" +) + +// handleTags handles any messages that have tags that will affect state. (e.g. +// 'account' tags.) +func handleTags(c *Client, e Event) { + if len(e.Tags) == 0 { + return + } + + account, ok := e.Tags.Get("account") + if !ok { + return + } + + c.state.Lock() + user := c.state.lookupUser(e.Source.ID()) + if user != nil { + user.Extras.Account = account + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +const ( + prefixTag byte = '@' + prefixTagValue byte = '=' + prefixUserTag byte = '+' + tagSeparator byte = ';' + maxTagLength int = 4094 // 4094 + @ and " " (space) = 4096, though space usually not included. +) + +// Tags represents the key-value pairs in IRCv3 message tags. The map contains +// the encoded message-tag values. If the tag is present, it may still be +// empty. See Tags.Get() and Tags.Set() for use with getting/setting +// information within the tags. +// +// Note that retrieving and setting tags are not concurrent safe. If this is +// necessary, you will need to implement it yourself. +type Tags map[string]string + +// ParseTags parses out the key-value map of tags. raw should only be the tag +// data, not a full message. For example: +// @aaa=bbb;ccc;example.com/ddd=eee +// NOT: +// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello +// +// Technically, there is a length limit of 4096, but the server should reject +// tag messages longer than this. +func ParseTags(raw string) (t Tags) { + t = make(Tags) + + if len(raw) > 0 && raw[0] == prefixTag { + raw = raw[1:] + } + + parts := strings.Split(raw, string(tagSeparator)) + var hasValue int + + for i := 0; i < len(parts); i++ { + hasValue = strings.IndexByte(parts[i], prefixTagValue) + + // The tag doesn't contain a value or has a splitter with no value. + if hasValue < 1 || len(parts[i]) < hasValue+1 { + if !validTag(parts[i]) { + continue + } + + t[parts[i]] = "" + continue + } + + // Check if tag key or decoded value are invalid. + // if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) { + // continue + // } + + t[parts[i][:hasValue]] = tagDecoder.Replace(parts[i][hasValue+1:]) + } + + return t +} + +// Len determines the length of the bytes representation of this tag map. This +// does not include the trailing space required when creating an event, but +// does include the tag prefix ("@"). +func (t Tags) Len() (length int) { + if t == nil { + return 0 + } + + return len(t.Bytes()) +} + +// Equals compares two Tags for equality. With the msgid IRCv3 spec +\ +// echo-message (amongst others), we may receive events that have msgid's, +// whereas our local events will not have the msgid. As such, don't compare +// all tags, only the necessary/important tags. +func (t Tags) Equals(tt Tags) bool { + // The only tag which is important at this time. + taccount, _ := t.Get("account") + ttaccount, _ := tt.Get("account") + return taccount == ttaccount +} + +// Keys returns a slice of (unsorted) tag keys. +func (t Tags) Keys() (keys []string) { + keys = make([]string, 0, t.Count()) + for key := range t { + keys = append(keys, key) + } + return keys +} + +// Count finds how many total tags that there are. +func (t Tags) Count() int { + if t == nil { + return 0 + } + + return len(t) +} + +// Bytes returns a []byte representation of this tag map, including the tag +// prefix ("@"). Note that this will return the tags sorted, regardless of +// the order of how they were originally parsed. +func (t Tags) Bytes() []byte { + if t == nil { + return []byte{} + } + + max := len(t) + if max == 0 { + return nil + } + + buffer := new(bytes.Buffer) + buffer.WriteByte(prefixTag) + + var current int + + // Sort the writing of tags so we can at least guarantee that they will + // be in order, and testable. + var names []string + for tagName := range t { + names = append(names, tagName) + } + sort.Strings(names) + + for i := 0; i < len(names); i++ { + // Trim at max allowed chars. + if (buffer.Len() + len(names[i]) + len(t[names[i]]) + 2) > maxTagLength { + return buffer.Bytes() + } + + buffer.WriteString(names[i]) + + // Write the value as necessary. + if len(t[names[i]]) > 0 { + buffer.WriteByte(prefixTagValue) + buffer.WriteString(t[names[i]]) + } + + // add the separator ";" between tags. + if current < max-1 { + buffer.WriteByte(tagSeparator) + } + + current++ + } + + return buffer.Bytes() +} + +// String returns a string representation of this tag map. +func (t Tags) String() string { + if t == nil { + return "" + } + + return string(t.Bytes()) +} + +// writeTo writes the necessary tag bytes to an io.Writer, including a trailing +// space-separator. +func (t Tags) writeTo(w io.Writer) (n int, err error) { + b := t.Bytes() + if len(b) == 0 { + return n, err + } + + n, err = w.Write(b) + if err != nil { + return n, err + } + + var j int + j, err = w.Write([]byte{eventSpace}) + n += j + + return n, err +} + +// tagDecode are encoded -> decoded pairs for replacement to decode. +var tagDecode = []string{ + "\\:", ";", + "\\s", " ", + "\\\\", "\\", + "\\r", "\r", + "\\n", "\n", +} +var tagDecoder = strings.NewReplacer(tagDecode...) + +// tagEncode are decoded -> encoded pairs for replacement to decode. +var tagEncode = []string{ + ";", "\\:", + " ", "\\s", + "\\", "\\\\", + "\r", "\\r", + "\n", "\\n", +} +var tagEncoder = strings.NewReplacer(tagEncode...) + +// Get returns the unescaped value of given tag key. Note that this is not +// concurrent safe. +func (t Tags) Get(key string) (tag string, success bool) { + if t == nil { + return "", false + } + + if _, ok := t[key]; ok { + tag = tagDecoder.Replace(t[key]) + success = true + } + + return tag, success +} + +// Set escapes given value and saves it as the value for given key. Note that +// this is not concurrent safe. +func (t Tags) Set(key, value string) error { + if t == nil { + t = make(Tags) + } + + if !validTag(key) { + return fmt.Errorf("tag key %q is invalid", key) + } + + value = tagEncoder.Replace(value) + + if len(value) > 0 && !validTagValue(value) { + return fmt.Errorf("tag value %q of key %q is invalid", value, key) + } + + // Check to make sure it's not too long here. + if (t.Len() + len(key) + len(value) + 2) > maxTagLength { + return fmt.Errorf("unable to set tag %q [value %q]: tags too long for message", key, value) + } + + t[key] = value + + return nil +} + +// Remove deletes the tag frwom the tag map. +func (t Tags) Remove(key string) (success bool) { + if t == nil { + return false + } + + if _, success = t[key]; success { + delete(t, key) + } + + return success +} + +// validTag validates an IRC tag. +func validTag(name string) bool { + if len(name) < 1 { + return false + } + + // Allow user tags to be passed to validTag. + if len(name) >= 2 && name[0] == prefixUserTag { + name = name[1:] + } + + for i := 0; i < len(name); i++ { + // A-Z, a-z, 0-9, -/._ + if (name[i] < 'A' || name[i] > 'Z') && (name[i] < 'a' || name[i] > 'z') && (name[i] < '-' || name[i] > '9') && name[i] != '_' { + return false + } + } + + return true +} + +// validTagValue valids a decoded IRC tag value. If the value is not decoded +// with tagDecoder first, it may be seen as invalid. +func validTagValue(value string) bool { + for i := 0; i < len(value); i++ { + // Don't allow any invisible chars within the tag, or semicolons. + if value[i] < '!' || value[i] > '~' || value[i] == ';' { + return false + } + } + return true +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/client.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/client.go new file mode 100644 index 0000000..db6ec08 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/client.go @@ -0,0 +1,792 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "log" + "net" + "os" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +// Client contains all of the information necessary to run a single IRC +// client. +type Client struct { + // Config represents the configuration. Please take extra caution in that + // entries in this are not edited while the client is connected, to prevent + // data races. This is NOT concurrent safe to update. + Config Config + // rx is a buffer of events waiting to be processed. + rx chan *Event + // tx is a buffer of events waiting to be sent. + tx chan *Event + // state represents the throw-away state for the irc session. + state *state + // initTime represents the creation time of the client. + initTime time.Time + // Handlers is a handler which manages internal and external handlers. + Handlers *Caller + // CTCP is a handler which manages internal and external CTCP handlers. + CTCP *CTCP + // Cmd contains various helper methods to interact with the server. + Cmd *Commands + // mu is the mux used for connections/disconnections from the server, + // so multiple threads aren't trying to connect at the same time, and + // vice versa. + mu sync.RWMutex + // stop is used to communicate with Connect(), letting it know that the + // client wishes to cancel/close. + stop context.CancelFunc + // conn is a net.Conn reference to the IRC server. If this is nil, it is + // safe to assume that we're not connected. If this is not nil, this + // means we're either connected, connecting, or cleaning up. This should + // be guarded with Client.mu. + conn *ircConn + // debug is used if a writer is supplied for Client.Config.Debugger. + debug *log.Logger +} + +// Config contains configuration options for an IRC client +type Config struct { + // Server is a host/ip of the server you want to connect to. This only + // has an affect during the dial process + Server string + // ServerPass is the server password used to authenticate. This only has + // an affect during the dial process. + ServerPass string + // Port is the port that will be used during server connection. This only + // has an affect during the dial process. + Port int + // Nick is an rfc-valid nickname used during connection. This only has an + // affect during the dial process. + Nick string + // User is the username/ident to use on connect. Ignored if an identd + // server is used. This only has an affect during the dial process. + User string + // Name is the "realname" that's used during connection. This only has an + // affect during the dial process. + Name string + // SASL contains the necessary authentication data to authenticate + // with SASL. See the documentation for SASLMech for what is currently + // supported. Capability tracking must be enabled for this to work, as + // this requires IRCv3 CAP handling. + SASL SASLMech + // WebIRC allows forwarding source user hostname/ip information to the server + // (if supported by the server) to ensure the source machine doesn't show as + // the source. See the WebIRC type for more information. + WebIRC WebIRC + // Bind is used to bind to a specific host or ip during the dial process + // when connecting to the server. This can be a hostname, however it must + // resolve to an IPv4/IPv6 address bindable on your system. Otherwise, + // you can simply use a IPv4/IPv6 address directly. This only has an + // affect during the dial process and will not work with DialerConnect(). + Bind string + // SSL allows dialing via TLS. See TLSConfig to set your own TLS + // configuration (e.g. to not force hostname checking). This only has an + // affect during the dial process. + SSL bool + // DisableSTS disables the use of automatic STS connection upgrades + // when the server supports STS. STS can also be disabled using the environment + // variable "GIRC_DISABLE_STS=true". As many clients may not propagate options + // like this back to the user, this allows to directly disable such automatic + // functionality. + DisableSTS bool + // DisableSTSFallback disables the "fallback" to a non-tls connection if the + // strict transport policy expires and the first attempt to reconnect back to + // the tls version fails. + DisableSTSFallback bool + // TLSConfig is an optional user-supplied tls configuration, used during + // socket creation to the server. SSL must be enabled for this to be used. + // This only has an affect during the dial process. + TLSConfig *tls.Config + // AllowFlood allows the client to bypass the rate limit of outbound + // messages. + AllowFlood bool + // GlobalFormat enables passing through all events which have trailing + // text through the color Fmt() function, so you don't have to wrap + // every response in the Fmt() method. + // + // Note that this only actually applies to PRIVMSG, NOTICE and TOPIC + // events, to ensure it doesn't clobber unwanted events. + GlobalFormat bool + // Debug is an optional, user supplied location to log the raw lines + // sent from the server, or other useful debug logs. Defaults to + // ioutil.Discard. For quick debugging, this could be set to os.Stdout. + Debug io.Writer + // Out is used to write out a prettified version of incoming events. For + // example, channel JOIN/PART, PRIVMSG/NOTICE, KICk, etc. Useful to get + // a brief output of the activity of the client. If you are looking to + // log raw messages, look at a handler and girc.ALLEVENTS and the relevant + // Event.Bytes() or Event.String() methods. + Out io.Writer + // RecoverFunc is called when a handler throws a panic. If RecoverFunc is + // set, the panic will be considered recovered, otherwise the client will + // panic. Set this to DefaultRecoverHandler if you don't want the client + // to panic, however you don't want to handle the panic yourself. + // DefaultRecoverHandler will log the panic to Debug or os.Stdout if + // Debug is unset. + RecoverFunc func(c *Client, e *HandlerError) + // SupportedCaps are the IRCv3 capabilities you would like the client to + // support on top of the ones which the client already supports (see + // cap.go for which ones the client enables by default). Only use this + // if you have not called DisableTracking(). The keys value gets passed + // to the server if supported. + SupportedCaps map[string][]string + // Version is the application version information that will be used in + // response to a CTCP VERSION, if default CTCP replies have not been + // overwritten or a VERSION handler was already supplied. + Version string + // PingDelay is the frequency between when the client sends a keep-alive + // PING to the server, and awaits a response (and times out if the server + // doesn't respond in time). This should be between 20-600 seconds. See + // Client.Latency() if you want to determine the delay between the server + // and the client. If this is set to -1, the client will not attempt to + // send client -> server PING requests. + PingDelay time.Duration + + // disableTracking disables all channel and user-level tracking. Useful + // for highly embedded scripts with single purposes. This has an exported + // method which enables this and ensures proper cleanup, see + // Client.DisableTracking(). + disableTracking bool + // HandleNickCollide when set, allows the client to handle nick collisions + // in a custom way. If unset, the client will attempt to append a + // underscore to the end of the nickname, in order to bypass using + // an invalid nickname. For example, if "test" is already in use, or is + // blocked by the network/a service, the client will try and use "test_", + // then it will attempt "test__", "test___", and so on. + // + // If HandleNickCollide returns an empty string, the client will not + // attempt to fix nickname collisions, and you must handle this yourself. + HandleNickCollide func(oldNick string) (newNick string) +} + +// WebIRC is useful when a user connects through an indirect method, such web +// clients, the indirect client sends its own IP address instead of sending the +// user's IP address unless WebIRC is implemented by both the client and the +// server. +// +// Client expectations: +// - Perform any proxy resolution. +// - Check the reverse DNS and forward DNS match. +// - Check the IP against suitable access controls (ipaccess, dnsbl, etc). +// +// More information: +// - https://ircv3.net/specs/extensions/webirc.html +// - https://kiwiirc.com/docs/webirc +type WebIRC struct { + // Password that authenticates the WEBIRC command from this client. + Password string + // Gateway or client type requesting spoof (cgiirc defaults to cgiirc, as an + // example). + Gateway string + // Hostname of user. + Hostname string + // Address either in IPv4 dotted quad notation (e.g. 192.0.0.2) or IPv6 + // notation (e.g. 1234:5678:9abc::def). IPv4-in-IPv6 addresses + // (e.g. ::ffff:192.0.0.2) should not be sent. + Address string +} + +// Params returns the arguments for the WEBIRC command that can be passed to the +// server. +func (w WebIRC) Params() []string { + return []string{w.Password, w.Gateway, w.Hostname, w.Address} +} + +// ErrInvalidConfig is returned when the configuration passed to the client +// is invalid. +type ErrInvalidConfig struct { + Conf Config // Conf is the configuration that was not valid. + err error +} + +func (e ErrInvalidConfig) Error() string { return "invalid configuration: " + e.err.Error() } + +// isValid checks some basic settings to ensure the config is valid. +func (conf *Config) isValid() error { + if conf.Server == "" { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("empty server")} + } + + // Default port to 6667 (the standard IRC port). + if conf.Port == 0 { + conf.Port = 6667 + } + + if conf.Port < 1 || conf.Port > 65535 { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (1-65535)")} + } + + if !IsValidNick(conf.Nick) { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")} + } + if !IsValidUser(conf.User) { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")} + } + + return nil +} + +// ErrNotConnected is returned if a method is used when the client isn't +// connected. +var ErrNotConnected = errors.New("client is not connected to server") + +// New creates a new IRC client with the specified server, name and config. +func New(config Config) *Client { + c := &Client{ + Config: config, + rx: make(chan *Event, 25), + tx: make(chan *Event, 25), + CTCP: newCTCP(), + initTime: time.Now(), + } + + c.Cmd = &Commands{c: c} + + if c.Config.PingDelay >= 0 && c.Config.PingDelay < (20*time.Second) { + c.Config.PingDelay = 20 * time.Second + } else if c.Config.PingDelay > (600 * time.Second) { + c.Config.PingDelay = 600 * time.Second + } + + envDebug, _ := strconv.ParseBool(os.Getenv("GIRC_DEBUG")) + if c.Config.Debug == nil { + if envDebug { + c.debug = log.New(os.Stderr, "debug:", log.Ltime|log.Lshortfile) + } else { + c.debug = log.New(io.Discard, "", 0) + } + } else { + if envDebug { + if c.Config.Debug != os.Stdout && c.Config.Debug != os.Stderr { + c.Config.Debug = io.MultiWriter(os.Stderr, c.Config.Debug) + } + } + c.debug = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile) + c.debug.Print("initializing debugging") + } + + envDisableSTS, _ := strconv.ParseBool((os.Getenv("GIRC_DISABLE_STS"))) + if envDisableSTS { + c.Config.DisableSTS = envDisableSTS + } + + // Setup the caller. + c.Handlers = newCaller(c.debug) + + // Give ourselves a new state. + c.state = &state{} + c.state.reset(true) + + // Register builtin handlers. + c.registerBuiltins() + + // Register default CTCP responses. + c.CTCP.addDefaultHandlers() + + return c +} + +// String returns a brief description of the current client state. +func (c *Client) String() string { + connected := c.IsConnected() + + return fmt.Sprintf( + "<Client init:%q handlers:%d connected:%t>", c.initTime.String(), c.Handlers.Len(), connected, + ) +} + +// TLSConnectionState returns the TLS connection state from tls.Conn{}, which +// is useful to return needed TLS fingerprint info, certificates, verify cert +// expiration dates, etc. Will only return an error if the underlying +// connection wasn't established using TLS (see ErrConnNotTLS), or if the +// client isn't connected. +func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.conn == nil { + return nil, ErrNotConnected + } + + c.conn.mu.RLock() + defer c.conn.mu.RUnlock() + + if !c.conn.connected { + return nil, ErrNotConnected + } + + if tlsConn, ok := c.conn.sock.(*tls.Conn); ok { + cs := tlsConn.ConnectionState() + return &cs, nil + } + + return nil, ErrConnNotTLS +} + +// ErrConnNotTLS is returned when Client.TLSConnectionState() is called, and +// the connection to the server wasn't made with TLS. +var ErrConnNotTLS = errors.New("underlying connection is not tls") + +// Close closes the network connection to the server, and sends a CLOSED +// event. This should cause Connect() to return with nil. This should be +// safe to call multiple times. See Connect()'s documentation on how +// handlers and goroutines are handled when disconnected from the server. +func (c *Client) Close() { + c.mu.RLock() + if c.stop != nil { + c.debug.Print("requesting client to stop") + c.stop() + } + c.mu.RUnlock() +} + +// Quit sends a QUIT message to the server with a given reason to close the +// connection. Underlying this event being sent, Client.Close() is called as well. +// This is different than just calling Client.Close() in that it provides a reason +// as to why the connection was closed (for bots to tell users the bot is restarting, +// or shutting down, etc). +// +// NOTE: servers may delay showing of QUIT reasons, until you've been connected to +// the server for a certain period of time (e.g. 5 minutes). Keep this in mind. +func (c *Client) Quit(reason string) { + c.Send(&Event{Command: QUIT, Params: []string{reason}}) +} + +// ErrEvent is an error returned when the server (or library) sends an ERROR +// message response. The string returned contains the trailing text from the +// message. +type ErrEvent struct { + Event *Event +} + +func (e *ErrEvent) Error() string { + if e.Event == nil { + return "unknown error occurred" + } + + return e.Event.Last() +} + +func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + c.debug.Print("starting execLoop") + defer c.debug.Print("closing execLoop") + + var event *Event + + for { + select { + case <-ctx.Done(): + // We've been told to exit, however we shouldn't bail on the + // current events in the queue that should be processed, as one + // may want to handle an ERROR, QUIT, etc. + c.debug.Printf("received signal to close, flushing %d events and executing", len(c.rx)) + for { + select { + case event = <-c.rx: + c.RunHandlers(event) + default: + goto done + } + } + + done: + wg.Done() + return + case event = <-c.rx: + if event != nil && event.Command == ERROR { + // Handles incoming ERROR responses. These are only ever sent + // by the server (with the exception that this library may use + // them as a lower level way of signalling to disconnect due + // to some other client-chosen error), and should always be + // followed up by the server disconnecting the client. If for + // some reason the server doesn't disconnect the client, or + // if this library is the source of the error, this should + // signal back up to the main connect loop, to disconnect. + errs <- &ErrEvent{Event: event} + + // Make sure to not actually exit, so we can let any handlers + // actually handle the ERROR event. + } + + c.RunHandlers(event) + } + } +} + +// DisableTracking disables all channel/user-level/CAP tracking, and clears +// all internal handlers. Useful for highly embedded scripts with single +// purposes. This cannot be un-done on a client. +func (c *Client) DisableTracking() { + c.debug.Print("disabling tracking") + c.Config.disableTracking = true + c.Handlers.clearInternal() + + c.state.Lock() + c.state.channels = nil + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) + + c.registerBuiltins() +} + +// Server returns the string representation of host+port pair for the connection. +func (c *Client) Server() string { + c.state.Lock() + defer c.state.Unlock() + + return c.server() +} + +// server returns the string representation of host+port pair for net.Conn, and +// takes into consideration STS. Must lock state mu first! +func (c *Client) server() string { + if c.state.sts.enabled() { + return net.JoinHostPort(c.Config.Server, strconv.Itoa(c.state.sts.upgradePort)) + } + return net.JoinHostPort(c.Config.Server, strconv.Itoa(c.Config.Port)) +} + +// Lifetime returns the amount of time that has passed since the client was +// created. +func (c *Client) Lifetime() time.Duration { + return time.Since(c.initTime) +} + +// Uptime is the time at which the client successfully connected to the +// server. +func (c *Client) Uptime() (up *time.Time, err error) { + if !c.IsConnected() { + return nil, ErrNotConnected + } + + c.mu.RLock() + c.conn.mu.RLock() + up = c.conn.connTime + c.conn.mu.RUnlock() + c.mu.RUnlock() + + return up, nil +} + +// ConnSince is the duration that has past since the client successfully +// connected to the server. +func (c *Client) ConnSince() (since *time.Duration, err error) { + if !c.IsConnected() { + return nil, ErrNotConnected + } + + c.mu.RLock() + c.conn.mu.RLock() + timeSince := time.Since(*c.conn.connTime) + c.conn.mu.RUnlock() + c.mu.RUnlock() + + return &timeSince, nil +} + +// IsConnected returns true if the client is connected to the server. +func (c *Client) IsConnected() bool { + c.mu.RLock() + if c.conn == nil { + c.mu.RUnlock() + return false + } + + c.conn.mu.RLock() + connected := c.conn.connected + c.conn.mu.RUnlock() + c.mu.RUnlock() + + return connected +} + +// GetNick returns the current nickname of the active connection. Panics if +// tracking is disabled. +func (c *Client) GetNick() string { + c.panicIfNotTracking() + + c.state.RLock() + defer c.state.RUnlock() + + if c.state.nick == "" { + return c.Config.Nick + } + return c.state.nick +} + +// GetID returns an RFC1459 compliant version of the current nickname. Panics +// if tracking is disabled. +func (c *Client) GetID() string { + return ToRFC1459(c.GetNick()) +} + +// GetIdent returns the current ident of the active connection. Panics if +// tracking is disabled. May be empty, as this is obtained from when we join +// a channel, as there is no other more efficient method to return this info. +func (c *Client) GetIdent() string { + c.panicIfNotTracking() + + c.state.RLock() + defer c.state.RUnlock() + + if c.state.ident == "" { + return c.Config.User + } + return c.state.ident +} + +// GetHost returns the current host of the active connection. Panics if +// tracking is disabled. May be empty, as this is obtained from when we join +// a channel, as there is no other more efficient method to return this info. +func (c *Client) GetHost() (host string) { + c.panicIfNotTracking() + + c.state.RLock() + host = c.state.host + c.state.RUnlock() + return host +} + +// ChannelList returns the (sorted) active list of channel names that the client +// is in. Panics if tracking is disabled. +func (c *Client) ChannelList() []string { + c.panicIfNotTracking() + + c.state.RLock() + channels := make([]string, 0, len(c.state.channels)) + for channel := range c.state.channels { + channels = append(channels, c.state.channels[channel].Name) + } + c.state.RUnlock() + sort.Strings(channels) + return channels +} + +// Channels returns the (sorted) active channels that the client is in. Panics +// if tracking is disabled. +func (c *Client) Channels() []*Channel { + c.panicIfNotTracking() + + c.state.RLock() + channels := make([]*Channel, 0, len(c.state.channels)) + for channel := range c.state.channels { + channels = append(channels, c.state.channels[channel].Copy()) + } + c.state.RUnlock() + + sort.Slice(channels, func(i, j int) bool { + return channels[i].Name < channels[j].Name + }) + return channels +} + +// UserList returns the (sorted) active list of nicknames that the client is +// tracking across all channels. Panics if tracking is disabled. +func (c *Client) UserList() []string { + c.panicIfNotTracking() + + c.state.RLock() + users := make([]string, 0, len(c.state.users)) + for user := range c.state.users { + users = append(users, c.state.users[user].Nick) + } + c.state.RUnlock() + sort.Strings(users) + return users +} + +// Users returns the (sorted) active users that the client is tracking across +// all channels. Panics if tracking is disabled. +func (c *Client) Users() []*User { + c.panicIfNotTracking() + + c.state.RLock() + users := make([]*User, 0, len(c.state.users)) + for user := range c.state.users { + users = append(users, c.state.users[user].Copy()) + } + c.state.RUnlock() + + sort.Slice(users, func(i, j int) bool { + return users[i].Nick < users[j].Nick + }) + return users +} + +// LookupChannel looks up a given channel in state. If the channel doesn't +// exist, nil is returned. Panics if tracking is disabled. +func (c *Client) LookupChannel(name string) (channel *Channel) { + c.panicIfNotTracking() + if name == "" { + return nil + } + + c.state.RLock() + channel = c.state.lookupChannel(name).Copy() + c.state.RUnlock() + return channel +} + +// LookupUser looks up a given user in state. If the user doesn't exist, nil +// is returned. Panics if tracking is disabled. +func (c *Client) LookupUser(nick string) (user *User) { + c.panicIfNotTracking() + if nick == "" { + return nil + } + + c.state.RLock() + user = c.state.lookupUser(nick).Copy() + c.state.RUnlock() + return user +} + +// IsInChannel returns true if the client is in channel. Panics if tracking +// is disabled. +func (c *Client) IsInChannel(channel string) (in bool) { + c.panicIfNotTracking() + + c.state.RLock() + _, in = c.state.channels[ToRFC1459(channel)] + c.state.RUnlock() + return in +} + +// GetServerOption retrieves a server capability setting that was retrieved +// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL). +// Will panic if used when tracking has been disabled. Examples of usage: +// +// nickLen, success := GetServerOption("MAXNICKLEN") +// +func (c *Client) GetServerOption(key string) (result string, ok bool) { + c.panicIfNotTracking() + + c.state.RLock() + result, ok = c.state.serverOptions[key] + c.state.RUnlock() + return result, ok +} + +// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC". +// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL). +// Will panic if used when tracking has been disabled. +func (c *Client) NetworkName() (name string) { + c.panicIfNotTracking() + + name, _ = c.GetServerOption("NETWORK") + return name +} + +// ServerVersion returns the server software version, if the server has +// supplied this information during connection. May be empty if the server +// does not support RPL_MYINFO. Will panic if used when tracking has been +// disabled. +func (c *Client) ServerVersion() (version string) { + c.panicIfNotTracking() + + version, _ = c.GetServerOption("VERSION") + return version +} + +// ServerMOTD returns the servers message of the day, if the server has sent +// it upon connect. Will panic if used when tracking has been disabled. +func (c *Client) ServerMOTD() (motd string) { + c.panicIfNotTracking() + + c.state.RLock() + motd = c.state.motd + c.state.RUnlock() + return motd +} + +// Latency is the latency between the server and the client. This is measured +// by determining the difference in time between when we ping the server, and +// when we receive a pong. +func (c *Client) Latency() (delta time.Duration) { + c.mu.RLock() + c.conn.mu.RLock() + delta = c.conn.lastPong.Sub(c.conn.lastPing) + c.conn.mu.RUnlock() + c.mu.RUnlock() + + if delta < 0 { + return 0 + } + + return delta +} + +// HasCapability checks if the client connection has the given capability. If +// you want the full list of capabilities, listen for the girc.CAP_ACK event. +// Will panic if used when tracking has been disabled. +func (c *Client) HasCapability(name string) (has bool) { + c.panicIfNotTracking() + + if !c.IsConnected() { + return false + } + + name = strings.ToLower(name) + + c.state.RLock() + for key := range c.state.enabledCap { + key = strings.ToLower(key) + if key == name { + has = true + break + } + } + c.state.RUnlock() + + return has +} + +// panicIfNotTracking will throw a panic when it's called, and tracking is +// disabled. Adds useful info like what function specifically, and where it +// was called from. +func (c *Client) panicIfNotTracking() { + if !c.Config.disableTracking { + return + } + + pc, _, _, _ := runtime.Caller(1) + fn := runtime.FuncForPC(pc) + _, file, line, _ := runtime.Caller(2) + + panic(fmt.Sprintf("%s used when tracking is disabled (caller %s:%d)", fn.Name(), file, line)) +} + +func (c *Client) debugLogEvent(e *Event, dropped bool) { + var prefix string + + if dropped { + prefix = "dropping event (disconnected):" + } else { + prefix = ">" + } + + if e.Sensitive { + c.debug.Printf(prefix, " %s ***redacted***", e.Command) + } else { + c.debug.Print(prefix, " ", StripRaw(e.String())) + } + + if c.Config.Out != nil { + if pretty, ok := e.Pretty(); ok { + fmt.Fprintln(c.Config.Out, StripRaw(pretty)) + } + } +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/commands.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/commands.go new file mode 100644 index 0000000..91a8b96 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/commands.go @@ -0,0 +1,367 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "errors" + "fmt" + "strconv" +) + +// Commands holds a large list of useful methods to interact with the server, +// and wrappers for common events. +type Commands struct { + c *Client +} + +// Nick changes the client nickname. +func (cmd *Commands) Nick(name string) { + cmd.c.Send(&Event{Command: NICK, Params: []string{name}}) +} + +// Join attempts to enter a list of IRC channels, at bulk if possible to +// prevent sending extensive JOIN commands. +func (cmd *Commands) Join(channels ...string) { + // We can join multiple channels at once, however we need to ensure that + // we are not exceeding the line length. (see maxLength) + max := maxLength - len(JOIN) - 1 + + var buffer string + + for i := 0; i < len(channels); i++ { + if len(buffer+","+channels[i]) > max { + cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) + buffer = "" + continue + } + + if buffer == "" { + buffer = channels[i] + } else { + buffer += "," + channels[i] + } + + if i == len(channels)-1 { + cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) + return + } + } +} + +// JoinKey attempts to enter an IRC channel with a password. +func (cmd *Commands) JoinKey(channel, password string) { + cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}}) +} + +// Part leaves an IRC channel. +func (cmd *Commands) Part(channels ...string) { + for i := 0; i < len(channels); i++ { + cmd.c.Send(&Event{Command: PART, Params: []string{channels[i]}}) + } +} + +// PartMessage leaves an IRC channel with a specified leave message. +func (cmd *Commands) PartMessage(channel, message string) { + cmd.c.Send(&Event{Command: PART, Params: []string{channel, message}}) +} + +// SendCTCP sends a CTCP request to target. Note that this method uses +// PRIVMSG specifically. ctcpType is the CTCP command, e.g. "FINGER", "TIME", +// "VERSION", etc. +func (cmd *Commands) SendCTCP(target, ctcpType, message string) { + out := EncodeCTCPRaw(ctcpType, message) + if out == "" { + panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message)) + } + + cmd.Message(target, out) +} + +// SendCTCPf sends a CTCP request to target using a specific format. Note that +// this method uses PRIVMSG specifically. ctcpType is the CTCP command, e.g. +// "FINGER", "TIME", "VERSION", etc. +func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) { + cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...)) +} + +// SendCTCPReplyf sends a CTCP response to target using a specific format. +// Note that this method uses NOTICE specifically. ctcpType is the CTCP +// command, e.g. "FINGER", "TIME", "VERSION", etc. +func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) { + cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...)) +} + +// SendCTCPReply sends a CTCP response to target. Note that this method uses +// NOTICE specifically. +func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) { + out := EncodeCTCPRaw(ctcpType, message) + if out == "" { + panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message)) + } + + cmd.Notice(target, out) +} + +// Message sends a PRIVMSG to target (either channel, service, or user). +func (cmd *Commands) Message(target, message string) { + cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target, message}}) +} + +// Messagef sends a formated PRIVMSG to target (either channel, service, or +// user). +func (cmd *Commands) Messagef(target, format string, a ...interface{}) { + cmd.Message(target, fmt.Sprintf(format, a...)) +} + +// ErrInvalidSource is returned when a method needs to know the origin of an +// event, however Event.Source is unknown (e.g. sent by the user, not the +// server.) +var ErrInvalidSource = errors.New("event has nil or invalid source address") + +// Reply sends a reply to channel or user, based on where the supplied event +// originated from. See also ReplyTo(). Panics if the incoming event has no +// source. +func (cmd *Commands) Reply(event Event, message string) { + if event.Source == nil { + panic(ErrInvalidSource) + } + + if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { + cmd.Message(event.Params[0], message) + return + } + + cmd.Message(event.Source.Name, message) +} + +// Replyf sends a reply to channel or user with a format string, based on +// where the supplied event originated from. See also ReplyTof(). Panics if +// the incoming event has no source. +func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) { + cmd.Reply(event, fmt.Sprintf(format, a...)) +} + +// ReplyTo sends a reply to a channel or user, based on where the supplied +// event originated from. ReplyTo(), when originating from a channel will +// default to replying with "<user>, <message>". See also Reply(). Panics if +// the incoming event has no source. +func (cmd *Commands) ReplyTo(event Event, message string) { + if event.Source == nil { + panic(ErrInvalidSource) + } + + if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { + cmd.Message(event.Params[0], event.Source.Name+", "+message) + return + } + + cmd.Message(event.Source.Name, message) +} + +// ReplyTof sends a reply to a channel or user with a format string, based +// on where the supplied event originated from. ReplyTo(), when originating +// from a channel will default to replying with "<user>, <message>". See +// also Replyf(). Panics if the incoming event has no source. +func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) { + cmd.ReplyTo(event, fmt.Sprintf(format, a...)) +} + +// Action sends a PRIVMSG ACTION (/me) to target (either channel, service, +// or user). +func (cmd *Commands) Action(target, message string) { + cmd.c.Send(&Event{ + Command: PRIVMSG, + Params: []string{target, fmt.Sprintf("\001ACTION %s\001", message)}, + }) +} + +// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel, +// service, or user). +func (cmd *Commands) Actionf(target, format string, a ...interface{}) { + cmd.Action(target, fmt.Sprintf(format, a...)) +} + +// Notice sends a NOTICE to target (either channel, service, or user). +func (cmd *Commands) Notice(target, message string) { + cmd.c.Send(&Event{Command: NOTICE, Params: []string{target, message}}) +} + +// Noticef sends a formated NOTICE to target (either channel, service, or +// user). +func (cmd *Commands) Noticef(target, format string, a ...interface{}) { + cmd.Notice(target, fmt.Sprintf(format, a...)) +} + +// SendRaw sends a raw string (or multiple) to the server, without carriage +// returns or newlines. Returns an error if one of the raw strings cannot be +// properly parsed. +func (cmd *Commands) SendRaw(raw ...string) error { + var event *Event + + for i := 0; i < len(raw); i++ { + event = ParseEvent(raw[i]) + if event == nil { + return errors.New("invalid event: " + raw[i]) + } + + cmd.c.Send(event) + } + + return nil +} + +// SendRawf sends a formated string back to the server, without carriage +// returns or newlines. +func (cmd *Commands) SendRawf(format string, a ...interface{}) error { + return cmd.SendRaw(fmt.Sprintf(format, a...)) +} + +// Topic sets the topic of channel to message. Does not verify the length +// of the topic. +func (cmd *Commands) Topic(channel, message string) { + cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, message}}) +} + +// Who sends a WHO query to the server, which will attempt WHOX by default. +// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This +// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with +// girc's builtin tracking functionality. +func (cmd *Commands) Who(users ...string) { + for i := 0; i < len(users); i++ { + cmd.c.Send(&Event{Command: WHO, Params: []string{users[i], "%tcuhnr,2"}}) + } +} + +// Whois sends a WHOIS query to the server, targeted at a specific user (or +// set of users). As WHOIS is a bit slower, you may want to use WHO for brief +// user info. +func (cmd *Commands) Whois(users ...string) { + for i := 0; i < len(users); i++ { + cmd.c.Send(&Event{Command: WHOIS, Params: []string{users[i]}}) + } +} + +// Ping sends a PING query to the server, with a specific identifier that +// the server should respond with. +func (cmd *Commands) Ping(id string) { + cmd.c.write(&Event{Command: PING, Params: []string{id}}) +} + +// Pong sends a PONG query to the server, with an identifier which was +// received from a previous PING query received by the client. +func (cmd *Commands) Pong(id string) { + cmd.c.write(&Event{Command: PONG, Params: []string{id}}) +} + +// Oper sends a OPER authentication query to the server, with a username +// and password. +func (cmd *Commands) Oper(user, pass string) { + cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true}) +} + +// Kick sends a KICK query to the server, attempting to kick nick from +// channel, with reason. If reason is blank, one will not be sent to the +// server. +func (cmd *Commands) Kick(channel, user, reason string) { + if reason != "" { + cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user, reason}}) + } + + cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}}) +} + +// Ban adds the +b mode on the given mask on a channel. +func (cmd *Commands) Ban(channel, mask string) { + cmd.Mode(channel, "+b", mask) +} + +// Unban removes the +b mode on the given mask on a channel. +func (cmd *Commands) Unban(channel, mask string) { + cmd.Mode(channel, "-b", mask) +} + +// Mode sends a mode change to the server which should be applied to target +// (usually a channel or user), along with a set of modes (generally "+m", +// "+mmmm", or "-m", where "m" is the mode you want to change). Params is only +// needed if the mode change requires a parameter (ban or invite-only exclude.) +func (cmd *Commands) Mode(target, modes string, params ...string) { + out := []string{target, modes} + out = append(out, params...) + + cmd.c.Send(&Event{Command: MODE, Params: out}) +} + +// Invite sends a INVITE query to the server, to invite nick to channel. +func (cmd *Commands) Invite(channel string, users ...string) { + for i := 0; i < len(users); i++ { + cmd.c.Send(&Event{Command: INVITE, Params: []string{users[i], channel}}) + } +} + +// Away sends a AWAY query to the server, suggesting that the client is no +// longer active. If reason is blank, Client.Back() is called. Also see +// Client.Back(). +func (cmd *Commands) Away(reason string) { + if reason == "" { + cmd.Back() + return + } + + cmd.c.Send(&Event{Command: AWAY, Params: []string{reason}}) +} + +// Back sends a AWAY query to the server, however the query is blank, +// suggesting that the client is active once again. Also see Client.Away(). +func (cmd *Commands) Back() { + cmd.c.Send(&Event{Command: AWAY}) +} + +// List sends a LIST query to the server, which will list channels and topics. +// Supports multiple channels at once, in hopes it will reduce extensive +// LIST queries to the server. Supply no channels to run a list against the +// entire server (warning, that may mean LOTS of channels!) +func (cmd *Commands) List(channels ...string) { + if len(channels) == 0 { + cmd.c.Send(&Event{Command: LIST}) + return + } + + // We can LIST multiple channels at once, however we need to ensure that + // we are not exceeding the line length. (see maxLength) + max := maxLength - len(JOIN) - 1 + + var buffer string + + for i := 0; i < len(channels); i++ { + if len(buffer+","+channels[i]) > max { + cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) + buffer = "" + continue + } + + if buffer == "" { + buffer = channels[i] + } else { + buffer += "," + channels[i] + } + + if i == len(channels)-1 { + cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) + return + } + } +} + +// Whowas sends a WHOWAS query to the server. amount is the amount of results +// you want back. +func (cmd *Commands) Whowas(user string, amount int) { + cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, strconv.Itoa(amount)}}) +} + +// Monitor sends a MONITOR query to the server. The results of the query +// depends on the given modifier, see https://ircv3.net/specs/core/monitor-3.2.html +func (cmd *Commands) Monitor(modifier rune, args ...string) { + cmd.c.Send(&Event{Command: MONITOR, Params: append([]string{string(modifier)}, args...)}) +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/conn.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/conn.go new file mode 100644 index 0000000..626a6dc --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/conn.go @@ -0,0 +1,635 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "bufio" + "context" + "crypto/tls" + "fmt" + "net" + "sync" + "time" +) + +// Messages are delimited with CR and LF line endings, we're using the last +// one to split the stream. Both are removed during parsing of the message. +const delim byte = '\n' + +var endline = []byte("\r\n") + +// ircConn represents an IRC network protocol connection, it consists of an +// Encoder and Decoder to manage i/o. +type ircConn struct { + io *bufio.ReadWriter + sock net.Conn + + mu sync.RWMutex + // lastWrite is used to keep track of when we last wrote to the server. + lastWrite time.Time + // lastActive is the last time the client was interacting with the server, + // excluding a few background commands (PING, PONG, WHO, etc). + lastActive time.Time + // writeDelay is used to keep track of rate limiting of events sent to + // the server. + writeDelay time.Duration + // connected is true if we're actively connected to a server. + connected bool + // connTime is the time at which the client has connected to a server. + connTime *time.Time + // lastPing is the last time that we pinged the server. + lastPing time.Time + // lastPong is the last successful time that we pinged the server and + // received a successful pong back. + lastPong time.Time +} + +// Dialer is an interface implementation of net.Dialer. Use this if you would +// like to implement your own dialer which the client will use when connecting. +type Dialer interface { + // Dial takes two arguments. Network, which should be similar to "tcp", + // "tdp6", "udp", etc -- as well as address, which is the hostname or ip + // of the network. Note that network can be ignored if your transport + // doesn't take advantage of network types. + Dial(network, address string) (net.Conn, error) +} + +// newConn sets up and returns a new connection to the server. +func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ircConn, error) { + if err := conf.isValid(); err != nil { + return nil, err + } + + var conn net.Conn + var err error + + if dialer == nil { + netDialer := &net.Dialer{Timeout: 5 * time.Second} + + if conf.Bind != "" { + var local *net.TCPAddr + local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0") + if err != nil { + return nil, err + } + + netDialer.LocalAddr = local + } + + dialer = netDialer + } + + if conn, err = dialer.Dial("tcp", addr); err != nil { + if sts.enabled() { + err = &ErrSTSUpgradeFailed{Err: err} + } + + if sts.expired() && !conf.DisableSTSFallback { + sts.lastFailed = time.Now() + sts.reset() + } + return nil, err + } + + if conf.SSL || sts.enabled() { + var tlsConn net.Conn + tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true) + if err != nil { + if sts.enabled() { + err = &ErrSTSUpgradeFailed{Err: err} + } + + if sts.expired() && !conf.DisableSTSFallback { + sts.lastFailed = time.Now() + sts.reset() + } + return nil, err + } + + conn = tlsConn + } + + ctime := time.Now() + + c := &ircConn{ + sock: conn, + connTime: &ctime, + connected: true, + } + c.newReadWriter() + + return c, nil +} + +func newMockConn(conn net.Conn) *ircConn { + ctime := time.Now() + c := &ircConn{ + sock: conn, + connTime: &ctime, + connected: true, + } + c.newReadWriter() + + return c +} + +// ErrParseEvent is returned when an event cannot be parsed with ParseEvent(). +type ErrParseEvent struct { + Line string +} + +func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line } + +func (c *ircConn) decode() (event *Event, err error) { + line, err := c.io.ReadString(delim) + if err != nil { + return nil, err + } + + if event = ParseEvent(line); event == nil { + return nil, ErrParseEvent{line} + } + + return event, nil +} + +func (c *ircConn) encode(event *Event) error { + if _, err := c.io.Write(event.Bytes()); err != nil { + return err + } + if _, err := c.io.Write(endline); err != nil { + return err + } + + return c.io.Flush() +} + +func (c *ircConn) newReadWriter() { + c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock)) +} + +func tlsHandshake(conn net.Conn, conf *tls.Config, server string, validate bool) (net.Conn, error) { + if conf == nil { + conf = &tls.Config{ServerName: server, InsecureSkipVerify: !validate} + } + + tlsConn := tls.Client(conn, conf) + return net.Conn(tlsConn), nil +} + +// Close closes the underlying socket. +func (c *ircConn) Close() error { + return c.sock.Close() +} + +// Connect attempts to connect to the given IRC server. Returns only when +// an error has occurred, or a disconnect was requested with Close(). Connect +// will only return once all client-based goroutines have been closed to +// ensure there are no long-running routines becoming backed up. +// +// Connect will wait for all non-goroutine handlers to complete on error/quit, +// however it will not wait for goroutine-based handlers. +// +// If this returns nil, this means that the client requested to be closed +// (e.g. Client.Close()). Connect will panic if called when the last call has +// not completed. +func (c *Client) Connect() error { + return c.internalConnect(nil, nil) +} + +// DialerConnect allows you to specify your own custom dialer which implements +// the Dialer interface. +// +// An example of using this library would be to take advantage of the +// golang.org/x/net/proxy library: +// +// proxyUrl, _ := proxyURI, err = url.Parse("socks5://1.2.3.4:8888") +// dialer, _ := proxy.FromURL(proxyURI, &net.Dialer{Timeout: 5 * time.Second}) +// _ := girc.DialerConnect(dialer) +func (c *Client) DialerConnect(dialer Dialer) error { + return c.internalConnect(nil, dialer) +} + +// MockConnect is used to implement mocking with an IRC server. Supply a net.Conn +// that will be used to spoof the server. A useful way to do this is to so +// net.Pipe(), pass one end into MockConnect(), and the other end into +// bufio.NewReader(). +// +// For example: +// +// client := girc.New(girc.Config{ +// Server: "dummy.int", +// Port: 6667, +// Nick: "test", +// User: "test", +// Name: "Testing123", +// }) +// +// in, out := net.Pipe() +// defer in.Close() +// defer out.Close() +// b := bufio.NewReader(in) +// +// go func() { +// if err := client.MockConnect(out); err != nil { +// panic(err) +// } +// }() +// +// defer client.Close(false) +// +// for { +// in.SetReadDeadline(time.Now().Add(300 * time.Second)) +// line, err := b.ReadString(byte('\n')) +// if err != nil { +// panic(err) +// } +// +// event := girc.ParseEvent(line) +// +// if event == nil { +// continue +// } +// +// // Do stuff with event here. +// } +func (c *Client) MockConnect(conn net.Conn) error { + return c.internalConnect(conn, nil) +} + +func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { +startConn: + // We want to be the only one handling connects/disconnects right now. + c.mu.Lock() + + if c.conn != nil { + panic("use of connect more than once") + } + + // Reset the state. + c.state.reset(false) + + addr := c.server() + + if mock == nil { + // Validate info, and actually make the connection. + c.debug.Printf("connecting to %s... (sts: %v, config-ssl: %v)", addr, c.state.sts.enabled(), c.Config.SSL) + conn, err := newConn(c.Config, dialer, addr, &c.state.sts) + if err != nil { + if _, ok := err.(*ErrSTSUpgradeFailed); ok { + if !c.state.sts.enabled() { + c.RunHandlers(&Event{Command: STS_ERR_FALLBACK}) + } + } + c.mu.Unlock() + return err + } + + c.conn = conn + } else { + c.conn = newMockConn(mock) + } + + var ctx context.Context + ctx, c.stop = context.WithCancel(context.Background()) + c.mu.Unlock() + + errs := make(chan error, 4) + var wg sync.WaitGroup + // 4 being the number of goroutines we need to finish when this function + // returns. + wg.Add(4) + go c.execLoop(ctx, errs, &wg) + go c.readLoop(ctx, errs, &wg) + go c.sendLoop(ctx, errs, &wg) + go c.pingLoop(ctx, errs, &wg) + + // Passwords first. + + if c.Config.WebIRC.Password != "" { + c.write(&Event{Command: WEBIRC, Params: c.Config.WebIRC.Params(), Sensitive: true}) + } + + if c.Config.ServerPass != "" { + c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}, Sensitive: true}) + } + + // List the IRCv3 capabilities, specifically with the max protocol we + // support. The IRCv3 specification doesn't directly state if this should + // be called directly before registration, or if it should be called + // after NICK/USER requests. It looks like non-supporting networks + // should ignore this, and some IRCv3 capable networks require this to + // occur before NICK/USER registration. + c.listCAP() + + // Then nickname. + c.write(&Event{Command: NICK, Params: []string{c.Config.Nick}}) + + // Then username and realname. + if c.Config.Name == "" { + c.Config.Name = c.Config.User + } + + c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*", c.Config.Name}}) + + // Send a virtual event allowing hooks for successful socket connection. + c.RunHandlers(&Event{Command: INITIALIZED, Params: []string{addr}}) + + // Wait for the first error. + var result error + select { + case <-ctx.Done(): + if !c.state.sts.beginUpgrade { + c.debug.Print("received request to close, beginning clean up") + } + c.RunHandlers(&Event{Command: CLOSED, Params: []string{addr}}) + case err := <-errs: + c.debug.Printf("received error, beginning cleanup: %v", err) + result = err + } + + // Make sure that the connection is closed if not already. + c.mu.RLock() + if c.stop != nil { + c.stop() + } + c.conn.mu.Lock() + c.conn.connected = false + _ = c.conn.Close() + c.conn.mu.Unlock() + c.mu.RUnlock() + + c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{addr}}) + + // Once we have our error/result, let all other functions know we're done. + c.debug.Print("waiting for all routines to finish") + + // Wait for all goroutines to finish. + wg.Wait() + close(errs) + + // This helps ensure that the end user isn't improperly using the client + // more than once. If they want to do this, they should be using multiple + // clients, not multiple instances of Connect(). + c.mu.Lock() + c.conn = nil + + if result == nil { + if c.state.sts.beginUpgrade { + c.state.sts.beginUpgrade = false + c.mu.Unlock() + goto startConn + } + + if c.state.sts.enabled() { + c.state.sts.persistenceReceived = time.Now() + } + } + c.mu.Unlock() + + return result +} + +// readLoop sets a timeout of 300 seconds, and then attempts to read from the +// IRC server. If there is an error, it calls Reconnect. +func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + c.debug.Print("starting readLoop") + defer c.debug.Print("closing readLoop") + + var event *Event + var err error + + for { + select { + case <-ctx.Done(): + wg.Done() + return + default: + _ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second)) + event, err = c.conn.decode() + if err != nil { + errs <- err + wg.Done() + return + } + + // Check if it's an echo-message. + if !c.Config.disableTracking { + event.Echo = (event.Command == PRIVMSG || event.Command == NOTICE) && + event.Source != nil && event.Source.ID() == c.GetID() + } + + c.rx <- event + } + } +} + +// Send sends an event to the server. Use Client.RunHandlers() if you are +// simply looking to trigger handlers with an event. +func (c *Client) Send(event *Event) { + var delay time.Duration + + if !c.Config.AllowFlood { + c.mu.RLock() + + // Drop the event early as we're disconnected, this way we don't have to wait + // the (potentially long) rate limit delay before dropping. + if c.conn == nil { + c.debugLogEvent(event, true) + c.mu.RUnlock() + return + } + + c.conn.mu.Lock() + delay = c.conn.rate(event.Len()) + c.conn.mu.Unlock() + c.mu.RUnlock() + } + + if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" && + (event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) { + event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1]) + } + + <-time.After(delay) + c.write(event) +} + +// write is the lower level function to write an event. It does not have a +// write-delay when sending events. +func (c *Client) write(event *Event) { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.conn == nil { + // Drop the event if disconnected. + c.debugLogEvent(event, true) + return + } + c.tx <- event +} + +// rate allows limiting events based on how frequent the event is being sent, +// as well as how many characters each event has. +func (c *ircConn) rate(chars int) time.Duration { + _time := time.Second + ((time.Duration(chars) * time.Second) / 100) + + if c.writeDelay += _time - time.Since(c.lastWrite); c.writeDelay < 0 { + c.writeDelay = 0 + } + + if c.writeDelay > (8 * time.Second) { + return _time + } + + return 0 +} + +func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + c.debug.Print("starting sendLoop") + defer c.debug.Print("closing sendLoop") + + var err error + + for { + select { + case event := <-c.tx: + // Check if tags exist on the event. If they do, and message-tags + // isn't a supported capability, remove them from the event. + if event.Tags != nil { + c.state.RLock() + var in bool + for i := 0; i < len(c.state.enabledCap); i++ { + if _, ok := c.state.enabledCap["message-tags"]; ok { + in = true + break + } + } + c.state.RUnlock() + + if !in { + event.Tags = Tags{} + } + } + + c.debugLogEvent(event, false) + + c.conn.mu.Lock() + c.conn.lastWrite = time.Now() + + if event.Command != PING && event.Command != PONG && event.Command != WHO { + c.conn.lastActive = c.conn.lastWrite + } + c.conn.mu.Unlock() + + // Write the raw line. + _, err = c.conn.io.Write(event.Bytes()) + if err == nil { + // And the \r\n. + _, err = c.conn.io.Write(endline) + if err == nil { + // Lastly, flush everything to the socket. + err = c.conn.io.Flush() + } + } + + if event.Command == QUIT { + c.Close() + wg.Done() + return + } + + if err != nil { + errs <- err + wg.Done() + return + } + case <-ctx.Done(): + wg.Done() + return + } + } +} + +// ErrTimedOut is returned when we attempt to ping the server, and timed out +// before receiving a PONG back. +type ErrTimedOut struct { + // TimeSinceSuccess is how long ago we received a successful pong. + TimeSinceSuccess time.Duration + // LastPong is the time we received our last successful pong. + LastPong time.Time + // LastPong is the last time we sent a pong request. + LastPing time.Time + // Delay is the configured delay between how often we send a ping request. + Delay time.Duration +} + +func (ErrTimedOut) Error() string { return "timed out waiting for a requested PING response" } + +func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + // Don't run the pingLoop if they want to disable it. + if c.Config.PingDelay <= 0 { + wg.Done() + return + } + + c.debug.Print("starting pingLoop") + defer c.debug.Print("closing pingLoop") + + c.conn.mu.Lock() + c.conn.lastPing = time.Now() + c.conn.lastPong = time.Now() + c.conn.mu.Unlock() + + tick := time.NewTicker(c.Config.PingDelay) + defer tick.Stop() + + started := time.Now() + past := false + pingSent := false + + for { + select { + case <-tick.C: + // Delay during connect to wait for the client to register, otherwise + // some ircd's will not respond (e.g. during SASL negotiation). + if !past { + if time.Since(started) < 30*time.Second { + continue + } + + past = true + } + + c.conn.mu.RLock() + if pingSent && time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) { + // It's 60 seconds over what out ping delay is, connection + // has probably dropped. + err := ErrTimedOut{ + TimeSinceSuccess: time.Since(c.conn.lastPong), + LastPong: c.conn.lastPong, + LastPing: c.conn.lastPing, + Delay: c.Config.PingDelay, + } + + c.conn.mu.RUnlock() + errs <- err + wg.Done() + return + } + c.conn.mu.RUnlock() + + c.conn.mu.Lock() + c.conn.lastPing = time.Now() + c.conn.mu.Unlock() + + c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) + pingSent = true + case <-ctx.Done(): + wg.Done() + return + } + } +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/constants.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/constants.go new file mode 100644 index 0000000..d605808 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/constants.go @@ -0,0 +1,351 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +// Standard CTCP based constants. +const ( + CTCP_ACTION = "ACTION" + CTCP_PING = "PING" + CTCP_PONG = "PONG" + CTCP_VERSION = "VERSION" + CTCP_USERINFO = "USERINFO" + CTCP_CLIENTINFO = "CLIENTINFO" + CTCP_SOURCE = "SOURCE" + CTCP_TIME = "TIME" + CTCP_FINGER = "FINGER" + CTCP_ERRMSG = "ERRMSG" +) + +// Emulated event commands used to allow easier hooks into the changing +// state of the client. +const ( + UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated. + UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated. + ALL_EVENTS = "*" // trigger on all events + CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port + INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port + DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not) + CLOSED = "CLIENT_CLOSED" // occurs when Client.Close() has been called + STS_UPGRADE_INIT = "STS_UPGRADE_INIT" // when an STS upgrade initially happens. + STS_ERR_FALLBACK = "STS_ERR_FALLBACK" // when an STS connection fails and fallbacks are supported. +) + +// User/channel prefixes :: RFC1459. +const ( + DefaultPrefixes = "(ov)@+" // the most common default prefixes + ModeAddPrefix = "+" // modes are being added + ModeDelPrefix = "-" // modes are being removed + + ChannelPrefix = "#" // regular channel + DistributedPrefix = "&" // distributed channel + OwnerPrefix = "~" // user owner +q (non-rfc) + AdminPrefix = "&" // user admin +a (non-rfc) + HalfOperatorPrefix = "%" // user half operator +h (non-rfc) + OperatorPrefix = "@" // user operator +o + VoicePrefix = "+" // user has voice +v +) + +// User modes :: RFC1459; section 4.2.3.2. +const ( + UserModeInvisible = "i" // invisible + UserModeOperator = "o" // server operator + UserModeServerNotices = "s" // user wants to receive server notices + UserModeWallops = "w" // user wants to receive wallops +) + +// Channel modes :: RFC1459; section 4.2.3.1. +const ( + ModeDefaults = "beI,k,l,imnpst" // the most common default modes + + ModeInviteOnly = "i" // only join with an invite + ModeKey = "k" // channel password + ModeLimit = "l" // user limit + ModeModerated = "m" // only voiced users and operators can talk + ModeOperator = "o" // operator + ModePrivate = "p" // private + ModeSecret = "s" // secret + ModeTopic = "t" // must be op to set topic + ModeVoice = "v" // speak during moderation mode + + ModeOwner = "q" // owner privileges (non-rfc) + ModeAdmin = "a" // admin privileges (non-rfc) + ModeHalfOperator = "h" // half-operator privileges (non-rfc) +) + +// IRC commands :: RFC2812; section 3 :: RFC2813; section 4. +const ( + ADMIN = "ADMIN" + AWAY = "AWAY" + CONNECT = "CONNECT" + DIE = "DIE" + ERROR = "ERROR" + INFO = "INFO" + INVITE = "INVITE" + ISON = "ISON" + JOIN = "JOIN" + KICK = "KICK" + KILL = "KILL" + LINKS = "LINKS" + LIST = "LIST" + LUSERS = "LUSERS" + MODE = "MODE" + MOTD = "MOTD" + NAMES = "NAMES" + NICK = "NICK" + NJOIN = "NJOIN" + NOTICE = "NOTICE" + OPER = "OPER" + PART = "PART" + PASS = "PASS" + PING = "PING" + PONG = "PONG" + PRIVMSG = "PRIVMSG" + QUIT = "QUIT" + REHASH = "REHASH" + RESTART = "RESTART" + SERVER = "SERVER" + SERVICE = "SERVICE" + SERVLIST = "SERVLIST" + SQUERY = "SQUERY" + SQUIT = "SQUIT" + STATS = "STATS" + SUMMON = "SUMMON" + TIME = "TIME" + TOPIC = "TOPIC" + TRACE = "TRACE" + USER = "USER" + USERHOST = "USERHOST" + USERS = "USERS" + VERSION = "VERSION" + WALLOPS = "WALLOPS" + WEBIRC = "WEBIRC" + WHO = "WHO" + WHOIS = "WHOIS" + WHOWAS = "WHOWAS" +) + +// Numeric IRC reply mapping :: RFC2812; section 5. +const ( + RPL_WELCOME = "001" + RPL_YOURHOST = "002" + RPL_CREATED = "003" + RPL_MYINFO = "004" + RPL_BOUNCE = "005" + RPL_ISUPPORT = "005" + RPL_USERHOST = "302" + RPL_ISON = "303" + RPL_AWAY = "301" + RPL_UNAWAY = "305" + RPL_NOWAWAY = "306" + RPL_WHOISUSER = "311" + RPL_WHOISSERVER = "312" + RPL_WHOISOPERATOR = "313" + RPL_WHOISIDLE = "317" + RPL_ENDOFWHOIS = "318" + RPL_WHOISCHANNELS = "319" + RPL_WHOWASUSER = "314" + RPL_ENDOFWHOWAS = "369" + RPL_LISTSTART = "321" + RPL_LIST = "322" + RPL_LISTEND = "323" //nolint:misspell // it's correct. + RPL_UNIQOPIS = "325" + RPL_CHANNELMODEIS = "324" + RPL_NOTOPIC = "331" + RPL_TOPIC = "332" + RPL_INVITING = "341" + RPL_SUMMONING = "342" + RPL_INVITELIST = "346" + RPL_ENDOFINVITELIST = "347" + RPL_EXCEPTLIST = "348" + RPL_ENDOFEXCEPTLIST = "349" + RPL_VERSION = "351" + RPL_WHOREPLY = "352" + RPL_ENDOFWHO = "315" + RPL_NAMREPLY = "353" + RPL_ENDOFNAMES = "366" + RPL_LINKS = "364" + RPL_ENDOFLINKS = "365" + RPL_BANLIST = "367" + RPL_ENDOFBANLIST = "368" + RPL_INFO = "371" + RPL_ENDOFINFO = "374" + RPL_MOTDSTART = "375" + RPL_MOTD = "372" + RPL_ENDOFMOTD = "376" + RPL_YOUREOPER = "381" + RPL_REHASHING = "382" + RPL_YOURESERVICE = "383" + RPL_TIME = "391" + RPL_USERSSTART = "392" + RPL_USERS = "393" + RPL_ENDOFUSERS = "394" + RPL_NOUSERS = "395" + RPL_TRACELINK = "200" + RPL_TRACECONNECTING = "201" + RPL_TRACEHANDSHAKE = "202" + RPL_TRACEUNKNOWN = "203" + RPL_TRACEOPERATOR = "204" + RPL_TRACEUSER = "205" + RPL_TRACESERVER = "206" + RPL_TRACESERVICE = "207" + RPL_TRACENEWTYPE = "208" + RPL_TRACECLASS = "209" + RPL_TRACERECONNECT = "210" + RPL_TRACELOG = "261" + RPL_TRACEEND = "262" + RPL_STATSLINKINFO = "211" + RPL_STATSCOMMANDS = "212" + RPL_ENDOFSTATS = "219" + RPL_STATSUPTIME = "242" + RPL_STATSOLINE = "243" + RPL_UMODEIS = "221" + RPL_SERVLIST = "234" + RPL_SERVLISTEND = "235" + RPL_LUSERCLIENT = "251" + RPL_LUSEROP = "252" + RPL_LUSERUNKNOWN = "253" + RPL_LUSERCHANNELS = "254" + RPL_LUSERME = "255" + RPL_ADMINME = "256" + RPL_ADMINLOC1 = "257" + RPL_ADMINLOC2 = "258" + RPL_ADMINEMAIL = "259" + RPL_TRYAGAIN = "263" + ERR_NOSUCHNICK = "401" + ERR_NOSUCHSERVER = "402" + ERR_NOSUCHCHANNEL = "403" + ERR_CANNOTSENDTOCHAN = "404" + ERR_TOOMANYCHANNELS = "405" + ERR_WASNOSUCHNICK = "406" + ERR_TOOMANYTARGETS = "407" + ERR_NOSUCHSERVICE = "408" + ERR_NOORIGIN = "409" + ERR_NORECIPIENT = "411" + ERR_NOTEXTTOSEND = "412" + ERR_NOTOPLEVEL = "413" + ERR_WILDTOPLEVEL = "414" + ERR_BADMASK = "415" + ERR_INPUTTOOLONG = "417" + ERR_UNKNOWNCOMMAND = "421" + ERR_NOMOTD = "422" + ERR_NOADMININFO = "423" + ERR_FILEERROR = "424" + ERR_NONICKNAMEGIVEN = "431" + ERR_ERRONEUSNICKNAME = "432" + ERR_NICKNAMEINUSE = "433" + ERR_NICKCOLLISION = "436" + ERR_UNAVAILRESOURCE = "437" + ERR_USERNOTINCHANNEL = "441" + ERR_NOTONCHANNEL = "442" + ERR_USERONCHANNEL = "443" + ERR_NOLOGIN = "444" + ERR_SUMMONDISABLED = "445" + ERR_USERSDISABLED = "446" + ERR_NOTREGISTERED = "451" + ERR_NEEDMOREPARAMS = "461" + ERR_ALREADYREGISTRED = "462" + ERR_NOPERMFORHOST = "463" + ERR_PASSWDMISMATCH = "464" + ERR_YOUREBANNEDCREEP = "465" + ERR_YOUWILLBEBANNED = "466" + ERR_KEYSET = "467" + ERR_CHANNELISFULL = "471" + ERR_UNKNOWNMODE = "472" + ERR_INVITEONLYCHAN = "473" + ERR_BANNEDFROMCHAN = "474" + ERR_BADCHANNELKEY = "475" + ERR_BADCHANMASK = "476" + ERR_NOCHANMODES = "477" + ERR_BANLISTFULL = "478" + ERR_NOPRIVILEGES = "481" + ERR_CHANOPRIVSNEEDED = "482" + ERR_CANTKILLSERVER = "483" + ERR_RESTRICTED = "484" + ERR_UNIQOPPRIVSNEEDED = "485" + ERR_NOOPERHOST = "491" + ERR_UMODEUNKNOWNFLAG = "501" + ERR_USERSDONTMATCH = "502" +) + +// IRCv3 commands and extensions :: http://ircv3.net/irc/. +const ( + AUTHENTICATE = "AUTHENTICATE" + MONITOR = "MONITOR" + STARTTLS = "STARTTLS" + + CAP = "CAP" + CAP_ACK = "ACK" + CAP_CLEAR = "CLEAR" + CAP_END = "END" + CAP_LIST = "LIST" + CAP_LS = "LS" + CAP_NAK = "NAK" + CAP_REQ = "REQ" + CAP_NEW = "NEW" + CAP_DEL = "DEL" + + CAP_CHGHOST = "CHGHOST" + CAP_AWAY = "AWAY" + CAP_ACCOUNT = "ACCOUNT" + CAP_TAGMSG = "TAGMSG" +) + +// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/. +const ( + RPL_LOGGEDIN = "900" + RPL_LOGGEDOUT = "901" + RPL_NICKLOCKED = "902" + RPL_SASLSUCCESS = "903" + ERR_SASLFAIL = "904" + ERR_SASLTOOLONG = "905" + ERR_SASLABORTED = "906" + ERR_SASLALREADY = "907" + RPL_SASLMECHS = "908" + RPL_STARTTLS = "670" + ERR_STARTTLS = "691" + RPL_MONONLINE = "730" + RPL_MONOFFLINE = "731" + RPL_MONLIST = "732" + RPL_ENDOFMONLIST = "733" + ERR_MONLISTFULL = "734" +) + +// Numeric IRC event mapping :: RFC2812; section 5.3. +const ( + RPL_STATSCLINE = "213" + RPL_STATSNLINE = "214" + RPL_STATSILINE = "215" + RPL_STATSKLINE = "216" + RPL_STATSQLINE = "217" + RPL_STATSYLINE = "218" + RPL_SERVICEINFO = "231" + RPL_ENDOFSERVICES = "232" + RPL_SERVICE = "233" + RPL_STATSVLINE = "240" + RPL_STATSLLINE = "241" + RPL_STATSHLINE = "244" + RPL_STATSSLINE = "245" + RPL_STATSPING = "246" + RPL_STATSBLINE = "247" + RPL_STATSDLINE = "250" + RPL_NONE = "300" + RPL_WHOISCHANOP = "316" + RPL_KILLDONE = "361" + RPL_CLOSING = "362" + RPL_CLOSEEND = "363" + RPL_INFOSTART = "373" + RPL_MYPORTIS = "384" + ERR_NOSERVICEHOST = "492" +) + +// Misc. +const ( + ERR_TOOMANYMATCHES = "416" // IRCNet. + RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode. + RPL_LOCALUSERS = "265" // aircd/hybrid/bahamut, used on freenode. + RPL_TOPICWHOTIME = "333" // ircu, used on freenode. + RPL_WHOSPCRPL = "354" // ircu, used on networks with WHOX support. + RPL_CREATIONTIME = "329" +) diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/ctcp.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/ctcp.go new file mode 100644 index 0000000..caa9429 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/ctcp.go @@ -0,0 +1,297 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "fmt" + "runtime" + "strings" + "sync" + "time" +) + +// ctcpDelim if the delimiter used for CTCP formatted events/messages. +const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages. + +// CTCPEvent is the necessary information from an IRC message. +type CTCPEvent struct { + // Origin is the original event that the CTCP event was decoded from. + Origin *Event `json:"origin"` + // Source is the author of the CTCP event. + Source *Source `json:"source"` + // Command is the type of CTCP event. E.g. PING, TIME, VERSION. + Command string `json:"command"` + // Text is the raw arguments following the command. + Text string `json:"text"` + // Reply is true if the CTCP event is intended to be a reply to a + // previous CTCP (e.g, if we sent one). + Reply bool `json:"reply"` +} + +// DecodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned +// if the incoming event does not have valid CTCP encoding. +func DecodeCTCP(e *Event) *CTCPEvent { + // http://www.irchelp.org/protocol/ctcpspec.html + + if e == nil { + return nil + } + + // Must be targeting a user/channel, AND trailing must have + // DELIM+TAG+DELIM minimum (at least 3 chars). + if len(e.Params) != 2 || len(e.Params[1]) < 3 { + return nil + } + + if e.Command != PRIVMSG && e.Command != NOTICE { + return nil + } + + if e.Params[1][0] != ctcpDelim || e.Params[1][len(e.Params[1])-1] != ctcpDelim { + return nil + } + + // Strip delimiters. + text := e.Params[1][1 : len(e.Params[1])-1] + + s := strings.IndexByte(text, eventSpace) + + // Check to see if it only contains a tag. + if s < 0 { + for i := 0; i < len(text); i++ { + // Check for A-Z, 0-9. + if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') { + return nil + } + } + + return &CTCPEvent{ + Origin: e, + Source: e.Source, + Command: text, + Reply: e.Command == NOTICE, + } + } + + // Loop through checking the tag first. + for i := 0; i < s; i++ { + // Check for A-Z, 0-9. + if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') { + return nil + } + } + + return &CTCPEvent{ + Origin: e, + Source: e.Source, + Command: text[0:s], + Text: text[s+1:], + Reply: e.Command == NOTICE, + } +} + +// EncodeCTCP encodes a CTCP event into a string, including delimiters. +func EncodeCTCP(ctcp *CTCPEvent) (out string) { + if ctcp == nil { + return "" + } + + return EncodeCTCPRaw(ctcp.Command, ctcp.Text) +} + +// EncodeCTCPRaw is much like EncodeCTCP, however accepts a raw command and +// string as input. +func EncodeCTCPRaw(cmd, text string) (out string) { + if cmd == "" { + return "" + } + + out = string(ctcpDelim) + cmd + + if len(text) > 0 { + out += string(eventSpace) + text + } + + return out + string(ctcpDelim) +} + +// CTCP handles the storage and execution of CTCP handlers against incoming +// CTCP events. +type CTCP struct { + // mu is the mutex that should be used when accessing any ctcp handlers. + mu sync.RWMutex + // handlers is a map of CTCP message -> functions. + handlers map[string]CTCPHandler +} + +// newCTCP returns a new clean CTCP handler. +func newCTCP() *CTCP { + return &CTCP{handlers: map[string]CTCPHandler{}} +} + +// call executes the necessary CTCP handler for the incoming event/CTCP +// command. +func (c *CTCP) call(client *Client, event *CTCPEvent) { + c.mu.RLock() + defer c.mu.RUnlock() + + // If they want to catch any panics, add to defer stack. + if client.Config.RecoverFunc != nil && event.Origin != nil { + defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3) + } + + // Support wildcard CTCP event handling. Gets executed first before + // regular event handlers. + if _, ok := c.handlers["*"]; ok { + c.handlers["*"](client, *event) + } + + if _, ok := c.handlers[event.Command]; !ok { + // If ACTION, don't do anything. + if event.Command == CTCP_ACTION { + return + } + + // Send a ERRMSG reply, if we know who sent it. + if event.Source != nil && IsValidNick(event.Source.ID()) { + client.Cmd.SendCTCPReply(event.Source.ID(), CTCP_ERRMSG, "that is an unknown CTCP query") + } + return + } + + c.handlers[event.Command](client, *event) +} + +// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty +// string is returned. +func (c *CTCP) parseCMD(cmd string) string { + // TODO: Needs proper testing. + // Check if wildcard. + if cmd == "*" { + return "*" + } + + cmd = strings.ToUpper(cmd) + + for i := 0; i < len(cmd); i++ { + // Check for A-Z, 0-9. + if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') { + return "" + } + } + + return cmd +} + +// Set saves handler for execution upon a matching incoming CTCP event. +// Use SetBg if the handler may take an extended period of time to execute. +// If you would like to have a handler which will catch ALL CTCP requests, +// simply use "*" in place of the command. +func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) { + if cmd = c.parseCMD(cmd); cmd == "" { + return + } + + c.mu.Lock() + c.handlers[cmd] = CTCPHandler(handler) + c.mu.Unlock() +} + +// SetBg is much like Set, however the handler is executed in the background, +// ensuring that event handling isn't hung during long running tasks. See Set +// for more information. +func (c *CTCP) SetBg(cmd string, handler func(client *Client, ctcp CTCPEvent)) { + c.Set(cmd, func(client *Client, ctcp CTCPEvent) { + go handler(client, ctcp) + }) +} + +// Clear removes currently setup handler for cmd, if one is set. +func (c *CTCP) Clear(cmd string) { + if cmd = c.parseCMD(cmd); cmd == "" { + return + } + + c.mu.Lock() + delete(c.handlers, cmd) + c.mu.Unlock() +} + +// ClearAll removes all currently setup and re-sets the default handlers. +func (c *CTCP) ClearAll() { + c.mu.Lock() + c.handlers = map[string]CTCPHandler{} + c.mu.Unlock() + + // Register necessary handlers. + c.addDefaultHandlers() +} + +// CTCPHandler is a type that represents the function necessary to +// implement a CTCP handler. +type CTCPHandler func(client *Client, ctcp CTCPEvent) + +// addDefaultHandlers adds some useful default CTCP response handlers. +func (c *CTCP) addDefaultHandlers() { + c.SetBg(CTCP_PING, handleCTCPPing) + c.SetBg(CTCP_PONG, handleCTCPPong) + c.SetBg(CTCP_VERSION, handleCTCPVersion) + c.SetBg(CTCP_SOURCE, handleCTCPSource) + c.SetBg(CTCP_TIME, handleCTCPTime) + c.SetBg(CTCP_FINGER, handleCTCPFinger) +} + +// handleCTCPPing replies with a ping and whatever was originally requested. +func handleCTCPPing(client *Client, ctcp CTCPEvent) { + if ctcp.Reply { + return + } + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_PING, ctcp.Text) +} + +// handleCTCPPong replies with a pong. +func handleCTCPPong(client *Client, ctcp CTCPEvent) { + if ctcp.Reply { + return + } + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_PONG, "") +} + +// handleCTCPVersion replies with the name of the client, Go version, as well +// as the os type (darwin, linux, windows, etc) and architecture type (x86, +// arm, etc). +func handleCTCPVersion(client *Client, ctcp CTCPEvent) { + if client.Config.Version != "" { + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_VERSION, client.Config.Version) + return + } + + client.Cmd.SendCTCPReplyf( + ctcp.Source.ID(), CTCP_VERSION, + "girc (github.com/lrstanley/girc) using %s (%s, %s)", + runtime.Version(), runtime.GOOS, runtime.GOARCH, + ) +} + +// handleCTCPSource replies with the public git location of this library. +func handleCTCPSource(client *Client, ctcp CTCPEvent) { + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://github.com/lrstanley/girc") +} + +// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's +// local time. +func handleCTCPTime(client *Client, ctcp CTCPEvent) { + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_TIME, ":"+time.Now().Format(time.RFC1123Z)) +} + +// handleCTCPFinger replies with the realname and idle time of the user. This +// is obsoleted by improvements to the IRC protocol, however still supported. +func handleCTCPFinger(client *Client, ctcp CTCPEvent) { + client.conn.mu.RLock() + active := client.conn.lastActive + client.conn.mu.RUnlock() + + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active))) +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/doc.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/doc.go new file mode 100644 index 0000000..e46a41a --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/doc.go @@ -0,0 +1,12 @@ +// Package girc provides a high level, yet flexible IRC library for use with +// interacting with IRC servers. girc has support for user/channel tracking, +// as well as a few other neat features (like auto-reconnect). +// +// Much of what girc can do, can also be disabled. The goal is to provide a +// solid API that you don't necessarily have to work with out of the box if +// you don't want to. +// +// See the examples below for a few brief and useful snippets taking +// advantage of girc, which should give you a general idea of how the API +// works. +package girc diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go new file mode 100644 index 0000000..7801615 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go @@ -0,0 +1,640 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "bytes" + "fmt" + "strings" + "time" +) + +const ( + eventSpace byte = ' ' // Separator. + maxLength int = 510 // Maximum length is 510 (2 for line endings). +) + +// cutCRFunc is used to trim CR characters from prefixes/messages. +func cutCRFunc(r rune) bool { + return r == '\r' || r == '\n' +} + +// ParseEvent takes a string and attempts to create a Event struct. Returns +// nil if the Event is invalid. +func ParseEvent(raw string) (e *Event) { + // Ignore empty events. + if raw = strings.TrimFunc(raw, cutCRFunc); len(raw) < 2 { + return nil + } + + var i, j int + e = &Event{Timestamp: time.Now()} + + if raw[0] == prefixTag { + // Tags end with a space. + i = strings.IndexByte(raw, eventSpace) + + if i < 2 { + return nil + } + + e.Tags = ParseTags(raw[1:i]) + if rawServerTime, ok := e.Tags.Get("time"); ok { + // Attempt to parse server-time. If we can't parse it, we just + // fall back to the time we received the message (locally.) + if stime, err := time.Parse(capServerTimeFormat, rawServerTime); err == nil { + e.Timestamp = stime.Local() + } + } + raw = raw[i+1:] + i = 0 + } + + if raw != "" && raw[0] == messagePrefix { + // Prefix ends with a space. + i = strings.IndexByte(raw, eventSpace) + + // Prefix string must not be empty if the indicator is present. + if i < 2 { + return nil + } + + e.Source = ParseSource(raw[1:i]) + + // Skip space at the end of the prefix. + i++ + } + + // Find end of command. + j = i + strings.IndexByte(raw[i:], eventSpace) + + if j < i { + // If there are no proceeding spaces, it's the only thing specified. + e.Command = strings.ToUpper(raw[i:]) + return e + } + + e.Command = strings.ToUpper(raw[i:j]) + + // Skip the space after the command. + j++ + + // Check if and where the trailing text is within the incoming line. + var lastIndex, trailerIndex int + for { + // We must loop through, as it's possible that the first message + // prefix is not actually what we want. (e.g, colons are commonly + // used within ISUPPORT to delegate things like CHANLIMIT or TARGMAX.) + lastIndex = trailerIndex + trailerIndex = strings.IndexByte(raw[j+lastIndex:], messagePrefix) + + if trailerIndex == -1 { + // No trailing argument found, assume the rest is just params. + e.Params = strings.Fields(raw[j:]) + return e + } + + // This means we found a prefix that was proceeded by a space, and + // it's good to assume this is the start of trailing text to the line. + if raw[j+lastIndex+trailerIndex-1] == eventSpace { + i = lastIndex + trailerIndex + break + } + + // Keep looping through until we either can't find any more prefixes, + // or we find the one we want. + trailerIndex += lastIndex + 1 + } + + // Set i to that of the substring we were using before, and where the + // trailing prefix is. + i = j + i + + // Check if we need to parse arguments. If so, take everything after the + // command, and right before the trailing prefix, and cut it up. + if i > j { + e.Params = strings.Fields(raw[j : i-1]) + } + + e.Params = append(e.Params, raw[i+1:]) + + return e +} + +// Event represents an IRC protocol message, see RFC1459 section 2.3.1 +// +// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf> +// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>] +// <command> :: <letter>{<letter>} | <number> <number> <number> +// <SPACE> :: ' '{' '} +// <params> :: <SPACE> [':' <trailing> | <middle> <params>] +// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL +// or CR or LF, the first of which may not be ':'> +// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or +// CR or LF> +// <crlf> :: CR LF +type Event struct { + // Source is the origin of the event. + Source *Source `json:"source"` + // Tags are the IRCv3 style message tags for the given event. Only use + // if network supported. + Tags Tags `json:"tags"` + // Timestamp is the time the event was received. This could optionally be + // used for client-stored sent messages too. If the server supports the + // "server-time" capability, this is synced to the UTC time that the server + // specifies. + Timestamp time.Time `json:"timestamp"` + // Command that represents the event, e.g. JOIN, PRIVMSG, KILL. + Command string `json:"command"` + // Params (parameters/args) to the command. Commonly nickname, channel, etc. + // The last item in the slice could potentially contain spaces (commonly + // referred to as the "trailing" parameter). + Params []string `json:"params"` + // Sensitive should be true if the message is sensitive (e.g. and should + // not be logged/shown in debugging output). + Sensitive bool `json:"sensitive"` + // If the event is an echo-message response. + Echo bool `json:"echo"` +} + +// Last returns the last parameter in Event.Params if it exists. +func (e *Event) Last() string { + if len(e.Params) >= 1 { + return e.Params[len(e.Params)-1] + } + return "" +} + +// Copy makes a deep copy of a given event, for use with allowing untrusted +// functions/handlers edit the event without causing potential issues with +// other handlers. +func (e *Event) Copy() *Event { + if e == nil { + return nil + } + + newEvent := &Event{ + Timestamp: e.Timestamp, + Command: e.Command, + Sensitive: e.Sensitive, + Echo: e.Echo, + } + + // Copy Source field, as it's a pointer and needs to be dereferenced. + if e.Source != nil { + newEvent.Source = e.Source.Copy() + } + + // Copy Params in order to dereference as well. + if e.Params != nil { + newEvent.Params = make([]string, len(e.Params)) + copy(newEvent.Params, e.Params) + } + + // Copy tags as necessary. + if e.Tags != nil { + newEvent.Tags = Tags{} + for k, v := range e.Tags { + newEvent.Tags[k] = v + } + } + + return newEvent +} + +// Equals compares two Events for equality. +func (e *Event) Equals(ev *Event) bool { + if e.Command != ev.Command || len(e.Params) != len(ev.Params) { + return false + } + + for i := 0; i < len(e.Params); i++ { + if e.Params[i] != ev.Params[i] { + return false + } + } + + if !e.Source.Equals(ev.Source) || !e.Tags.Equals(ev.Tags) { + return false + } + + return true +} + +// Len calculates the length of the string representation of event. Note that +// this will return the true length (even if longer than what IRC supports), +// which may be useful if you are trying to check and see if a message is +// too long, to trim it down yourself. +func (e *Event) Len() (length int) { + if e.Tags != nil { + // Include tags and trailing space. + length = e.Tags.Len() + 1 + } + if e.Source != nil { + // Include prefix and trailing space. + length += e.Source.Len() + 2 + } + + length += len(e.Command) + + if len(e.Params) > 0 { + // Spaces before each param. + length += len(e.Params) + + for i := 0; i < len(e.Params); i++ { + length += len(e.Params[i]) + + // If param contains a space or it's empty, it's trailing, so it should be + // prefixed with a colon (:). + if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") { + length++ + } + } + } + + return +} + +// Bytes returns a []byte representation of event. Strips all newlines and +// carriage returns. +// +// Per RFC2812 section 2.3, messages should not exceed 512 characters in +// length. This method forces that limit by discarding any characters +// exceeding the length limit. +func (e *Event) Bytes() []byte { + buffer := new(bytes.Buffer) + + // Tags. + if e.Tags != nil { + e.Tags.writeTo(buffer) + } + + // Event prefix. + if e.Source != nil { + buffer.WriteByte(messagePrefix) + e.Source.writeTo(buffer) + buffer.WriteByte(eventSpace) + } + + // Command is required. + buffer.WriteString(e.Command) + + // Space separated list of arguments. + if len(e.Params) > 0 { + for i := 0; i < len(e.Params); i++ { + if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") { + buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i]) + continue + } + buffer.WriteString(string(eventSpace) + e.Params[i]) + } + } + + // We need the limit the buffer length. + if buffer.Len() > (maxLength) { + buffer.Truncate(maxLength) + } + + // If we truncated in the middle of a utf8 character, we need to remove + // the other (now invalid) bytes. + out := bytes.ToValidUTF8(buffer.Bytes(), nil) + + // Strip newlines and carriage returns. + for i := 0; i < len(out); i++ { + if out[i] == '\n' || out[i] == '\r' { + out = append(out[:i], out[i+1:]...) + i-- // Decrease the index so we can pick up where we left off. + } + } + + return out +} + +// String returns a string representation of this event. Strips all newlines +// and carriage returns. +func (e *Event) String() string { + return string(e.Bytes()) +} + +// Pretty returns a prettified string of the event. If the event doesn't +// support prettification, ok is false. Pretty is not just useful to make +// an event prettier, but also to filter out events that most don't visually +// see in normal IRC clients. e.g. most clients don't show WHO queries. +func (e *Event) Pretty() (out string, ok bool) { + if e.Sensitive || e.Echo { + return "", false + } + + if e.Command == ERROR { + return fmt.Sprintf("[*] an error occurred: %s", e.Last()), true + } + + if e.Source == nil { + if e.Command != PRIVMSG && e.Command != NOTICE { + return "", false + } + + if len(e.Params) > 0 { + return fmt.Sprintf("[>] writing %s", e.String()), true + } + + return "", false + } + + if e.Command == INITIALIZED { + return fmt.Sprintf("[*] connection to %s initialized", e.Last()), true + } + + if e.Command == CONNECTED { + return fmt.Sprintf("[*] successfully connected to %s", e.Last()), true + } + + if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 { + if ctcp := DecodeCTCP(e); ctcp != nil { + if ctcp.Reply { + return + } + + if ctcp.Command == CTCP_ACTION { + return fmt.Sprintf("[%s] **%s** %s", strings.Join(e.Params[0:len(e.Params)-1], ","), ctcp.Source.Name, ctcp.Text), true + } + + return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true + } + + var source string + if e.Command == PRIVMSG { + source = fmt.Sprintf("(%s)", e.Source.Name) + } else { // NOTICE + source = fmt.Sprintf("--%s--", e.Source.Name) + } + + return fmt.Sprintf("[%s] %s %s", strings.Join(e.Params[0:len(e.Params)-1], ","), source, e.Last()), true + } + + if e.Command == RPL_MOTD || e.Command == RPL_MOTDSTART || + e.Command == RPL_WELCOME || e.Command == RPL_YOURHOST || + e.Command == RPL_CREATED || e.Command == RPL_LUSERCLIENT { + return "[*] " + e.Last(), true + } + + if e.Command == JOIN && len(e.Params) > 0 { + return fmt.Sprintf("[*] %s (%s) has joined %s", e.Source.Name, e.Source.Host, e.Params[0]), true + } + + if e.Command == PART && len(e.Params) > 0 { + return fmt.Sprintf("[*] %s (%s) has left %s (%s)", e.Source.Name, e.Source.Host, e.Params[0], e.Last()), true + } + + if e.Command == QUIT { + return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Last()), true + } + + if e.Command == INVITE && len(e.Params) == 1 { + return fmt.Sprintf("[*] %s invited to %s by %s", e.Params[0], e.Last(), e.Source.Name), true + } + + if e.Command == KICK && len(e.Params) >= 2 { + return fmt.Sprintf("[%s] *** %s has kicked %s: %s", e.Params[0], e.Source.Name, e.Params[1], e.Last()), true + } + + if e.Command == NICK { + return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Last()), true + } + + if e.Command == TOPIC && len(e.Params) >= 2 { + return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[0], e.Source.Name, e.Last()), true + } + + if e.Command == RPL_TOPIC && len(e.Params) > 0 { + if len(e.Params) >= 2 { + return fmt.Sprintf("[*] topic for %s is: %s", e.Params[1], e.Last()), true + } + return fmt.Sprintf("[*] topic for %s is: %s", e.Params[0], e.Last()), true + } + + if e.Command == MODE && len(e.Params) > 2 { + return fmt.Sprintf("[%s] *** %s set modes: %s", e.Params[0], e.Source.Name, strings.Join(e.Params[1:], " ")), true + } + + if e.Command == CAP_AWAY { + if len(e.Params) > 0 { + return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Last()), true + } + + return fmt.Sprintf("[*] %s is no longer away", e.Source.Name), true + } + + if e.Command == CAP_CHGHOST && len(e.Params) == 2 { + return fmt.Sprintf("[*] %s has changed their host to %s (was %s)", e.Source.Name, e.Params[1], e.Source.Host), true + } + + if e.Command == CAP_ACCOUNT && len(e.Params) == 1 { + if e.Params[0] == "*" { + return fmt.Sprintf("[*] %s has become un-authenticated", e.Source.Name), true + } + + return fmt.Sprintf("[*] %s has authenticated for account: %s", e.Source.Name, e.Params[0]), true + } + + if e.Command == CAP && len(e.Params) >= 2 && e.Params[1] == CAP_ACK { + return "[*] enabling capabilities: " + e.Last(), true + } + + return "", false +} + +// IsAction checks to see if the event is an ACTION (/me). +func (e *Event) IsAction() bool { + if e.Command != PRIVMSG { + return false + } + + ok, ctcp := e.IsCTCP() + return ok && ctcp.Command == CTCP_ACTION +} + +// IsCTCP checks to see if the event is a CTCP event, and if so, returns the +// converted CTCP event. +func (e *Event) IsCTCP() (ok bool, ctcp *CTCPEvent) { + ctcp = DecodeCTCP(e) + return ctcp != nil, ctcp +} + +// IsFromChannel checks to see if a message was from a channel (rather than +// a private message). +func (e *Event) IsFromChannel() bool { + if e.Source == nil || (e.Command != PRIVMSG && e.Command != NOTICE) || len(e.Params) < 1 { + return false + } + + if !IsValidChannel(e.Params[0]) { + return false + } + + return true +} + +// IsFromUser checks to see if a message was from a user (rather than a +// channel). +func (e *Event) IsFromUser() bool { + if e.Source == nil || (e.Command != PRIVMSG && e.Command != NOTICE) || len(e.Params) < 1 { + return false + } + + if !IsValidNick(e.Params[0]) { + return false + } + + return true +} + +// StripAction returns the stripped version of the action encoding from a +// PRIVMSG ACTION (/me). +func (e *Event) StripAction() string { + if !e.IsAction() { + return e.Last() + } + + msg := e.Last() + return msg[8 : len(msg)-1] +} + +const ( + messagePrefix byte = ':' // Prefix or last argument. + prefixIdent byte = '!' // Username. + prefixHost byte = '@' // Hostname. +) + +// Source represents the sender of an IRC event, see RFC1459 section 2.3.1. +// <servername> | <nick> [ '!' <user> ] [ '@' <host> ] +type Source struct { + // Name is the nickname, server name, or service name, in its original + // non-rfc1459 form. + Name string `json:"name"` + // Ident is commonly known as the "user". + Ident string `json:"ident"` + // Host is the hostname or IP address of the user/service. Is not accurate + // due to how IRC servers can spoof hostnames. + Host string `json:"host"` +} + +// ID is the nickname, server name, or service name, in it's converted +// and comparable) form. +func (s *Source) ID() string { + return ToRFC1459(s.Name) +} + +// Equals compares two Sources for equality. +func (s *Source) Equals(ss *Source) bool { + if s == nil && ss == nil { + return true + } + if s != nil && ss == nil || s == nil && ss != nil { + return false + } + if s.ID() != ss.ID() || s.Ident != ss.Ident || s.Host != ss.Host { + return false + } + return true +} + +// Copy returns a deep copy of Source. +func (s *Source) Copy() *Source { + if s == nil { + return nil + } + + newSource := &Source{ + Name: s.Name, + Ident: s.Ident, + Host: s.Host, + } + + return newSource +} + +// ParseSource takes a string and attempts to create a Source struct. +func ParseSource(raw string) (src *Source) { + src = new(Source) + + user := strings.IndexByte(raw, prefixIdent) + host := strings.IndexByte(raw, prefixHost) + + switch { + case user > 0 && host > user: + src.Name = raw[:user] + src.Ident = raw[user+1 : host] + src.Host = raw[host+1:] + case user > 0: + src.Name = raw[:user] + src.Ident = raw[user+1:] + case host > 0: + src.Name = raw[:host] + src.Host = raw[host+1:] + default: + src.Name = raw + } + + return src +} + +// Len calculates the length of the string representation of prefix +func (s *Source) Len() (length int) { + length = len(s.Name) + if len(s.Ident) > 0 { + length = 1 + length + len(s.Ident) + } + if len(s.Host) > 0 { + length = 1 + length + len(s.Host) + } + + return +} + +// Bytes returns a []byte representation of source. +func (s *Source) Bytes() []byte { + buffer := new(bytes.Buffer) + s.writeTo(buffer) + + return buffer.Bytes() +} + +// String returns a string representation of source. +func (s *Source) String() (out string) { + out = s.Name + if len(s.Ident) > 0 { + out = out + string(prefixIdent) + s.Ident + } + if len(s.Host) > 0 { + out = out + string(prefixHost) + s.Host + } + + return +} + +// IsHostmask returns true if source looks like a user hostmask. +func (s *Source) IsHostmask() bool { + return len(s.Ident) > 0 && len(s.Host) > 0 +} + +// IsServer returns true if this source looks like a server name. +func (s *Source) IsServer() bool { + return s.Ident == "" && s.Host == "" +} + +// writeTo is an utility function to write the source to the bytes.Buffer +// in Event.String(). +func (s *Source) writeTo(buffer *bytes.Buffer) { + buffer.WriteString(s.Name) + if len(s.Ident) > 0 { + buffer.WriteByte(prefixIdent) + buffer.WriteString(s.Ident) + } + if len(s.Host) > 0 { + buffer.WriteByte(prefixHost) + buffer.WriteString(s.Host) + } +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/format.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/format.go new file mode 100644 index 0000000..85e3e38 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/format.go @@ -0,0 +1,352 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "bytes" + "fmt" + "regexp" + "strings" +) + +const ( + fmtOpenChar = '{' + fmtCloseChar = '}' +) + +var fmtColors = map[string]int{ + "white": 0, + "black": 1, + "blue": 2, + "navy": 2, + "green": 3, + "red": 4, + "brown": 5, + "maroon": 5, + "purple": 6, + "gold": 7, + "olive": 7, + "orange": 7, + "yellow": 8, + "lightgreen": 9, + "lime": 9, + "teal": 10, + "cyan": 11, + "lightblue": 12, + "royal": 12, + "fuchsia": 13, + "lightpurple": 13, + "pink": 13, + "gray": 14, + "grey": 14, + "lightgrey": 15, + "silver": 15, +} + +var fmtCodes = map[string]string{ + "bold": "\x02", + "b": "\x02", + "italic": "\x1d", + "i": "\x1d", + "reset": "\x0f", + "r": "\x0f", + "clear": "\x03", + "c": "\x03", // Clears formatting. + "reverse": "\x16", + "underline": "\x1f", + "ul": "\x1f", + "ctcp": "\x01", // CTCP/ACTION delimiter. +} + +// Fmt takes format strings like "{red}" or "{red,blue}" (for background +// colors) and turns them into the resulting ASCII format/color codes for IRC. +// See format.go for the list of supported format codes allowed. +// +// For example: +// +// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}")) +func Fmt(text string) string { + var last = -1 + for i := 0; i < len(text); i++ { + if text[i] == fmtOpenChar { + last = i + continue + } + + if text[i] == fmtCloseChar && last > -1 { + code := strings.ToLower(text[last+1 : i]) + + // Check to see if they're passing in a second (background) color + // as {fgcolor,bgcolor}. + var secondary string + if com := strings.Index(code, ","); com > -1 { + secondary = code[com+1:] + code = code[:com] + } + + var repl string + + if color, ok := fmtColors[code]; ok { + repl = fmt.Sprintf("\x03%02d", color) + } + + if repl != "" && secondary != "" { + if color, ok := fmtColors[secondary]; ok { + repl += fmt.Sprintf(",%02d", color) + } + } + + if repl == "" { + if fmtCode, ok := fmtCodes[code]; ok { + repl = fmtCode + } + } + + next := len(text[:last]+repl) - 1 + text = text[:last] + repl + text[i+1:] + last = -1 + i = next + continue + } + + if last > -1 { + // A-Z, a-z, and "," + if text[i] != ',' && (text[i] < 'A' || text[i] > 'Z') && (text[i] < 'a' || text[i] > 'z') { + last = -1 + continue + } + } + } + + return text +} + +// TrimFmt strips all "{fmt}" formatting strings from the input text. +// See Fmt() for more information. +func TrimFmt(text string) string { + for color := range fmtColors { + text = strings.ReplaceAll(text, string(fmtOpenChar)+color+string(fmtCloseChar), "") + } + for code := range fmtCodes { + text = strings.ReplaceAll(text, string(fmtOpenChar)+code+string(fmtCloseChar), "") + } + + return text +} + +// This is really the only fastest way of doing this (marginally better than +// actually trying to parse it manually.) +var reStripColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)?`) + +// StripRaw tries to strip all ASCII format codes that are used for IRC. +// Primarily, foreground/background colors, and other control bytes like +// reset, bold, italic, reverse, etc. This also is done in a specific way +// in order to ensure no truncation of other non-irc formatting. +func StripRaw(text string) string { + text = reStripColor.ReplaceAllString(text, "") + + for _, code := range fmtCodes { + text = strings.ReplaceAll(text, code, "") + } + + return text +} + +// IsValidChannel validates if channel is an RFC compliant channel or not. +// +// NOTE: If you are using this to validate a channel that contains a channel +// ID, (!<channelid>NAME), this only supports the standard 5 character length. +// +// NOTE: If you do not need to validate against servers that support unicode, +// you may want to ensure that all channel chars are within the range of +// all ASCII printable chars. This function will NOT do that for +// compatibility reasons. +// +// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring +// [ ":" chanstring ] +// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B +// chanstring = / 0x2D-0x39 / 0x3B-0xFF +// ; any octet except NUL, BELL, CR, LF, " ", "," and ":" +// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 ) +func IsValidChannel(channel string) bool { + if len(channel) <= 1 || len(channel) > 50 { + return false + } + + // #, +, !<channelid>, ~, or & + // Including "*" and "~" in the prefix list, as these are commonly used + // (e.g. ZNC.) + if bytes.IndexByte([]byte{'!', '#', '&', '*', '~', '+'}, channel[0]) == -1 { + return false + } + + // !<channelid> -- not very commonly supported, but we'll check it anyway. + // The ID must be 5 chars. This means min-channel size should be: + // 1 (prefix) + 5 (id) + 1 (+, channel name) + // On some networks, this may be extended with ISUPPORT capabilities, + // however this is extremely uncommon. + if channel[0] == '!' { + if len(channel) < 7 { + return false + } + + // check for valid ID + for i := 1; i < 6; i++ { + if (channel[i] < '0' || channel[i] > '9') && (channel[i] < 'A' || channel[i] > 'Z') { + return false + } + } + } + + // Check for invalid octets here. + bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A} + for i := 1; i < len(channel); i++ { + if bytes.IndexByte(bad, channel[i]) != -1 { + return false + } + } + + return true +} + +// IsValidNick validates an IRC nickname. Note that this does not validate +// IRC nickname length. +// +// nickname = ( letter / special ) *8( letter / digit / special / "-" ) +// letter = 0x41-0x5A / 0x61-0x7A +// digit = 0x30-0x39 +// special = 0x5B-0x60 / 0x7B-0x7D +func IsValidNick(nick string) bool { + if nick == "" { + return false + } + + // Check the first index. Some characters aren't allowed for the first + // index of an IRC nickname. + if (nick[0] < 'A' || nick[0] > '}') && nick[0] != '?' { + // a-z, A-Z, '_\[]{}^|', and '?' in the case of znc. + return false + } + + for i := 1; i < len(nick); i++ { + if (nick[i] < 'A' || nick[i] > '}') && (nick[i] < '0' || nick[i] > '9') && nick[i] != '-' { + // a-z, A-Z, 0-9, -, and _\[]{}^| + return false + } + } + + return true +} + +// IsValidUser validates an IRC ident/username. Note that this does not +// validate IRC ident length. +// +// The validation checks are much like what characters are allowed with an +// IRC nickname (see IsValidNick()), however an ident/username can: +// +// 1. Must either start with alphanumberic char, or "~" then alphanumberic +// char. +// +// 2. Contain a "." (period), for use with "first.last". Though, this may +// not be supported on all networks. Some limit this to only a single period. +// +// Per RFC: +// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF ) +// ; any octet except NUL, CR, LF, " " and "@" +func IsValidUser(name string) bool { + if name == "" { + return false + } + + // "~" is prepended (commonly) if there was no ident server response. + if name[0] == '~' { + // Means name only contained "~". + if len(name) < 2 { + return false + } + + name = name[1:] + } + + // Check to see if the first index is alphanumeric. + if (name[0] < 'A' || name[0] > 'Z') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') { + return false + } + + for i := 1; i < len(name); i++ { + if (name[i] < 'A' || name[i] > '}') && (name[i] < '0' || name[i] > '9') && name[i] != '-' && name[i] != '.' { + // a-z, A-Z, 0-9, -, and _\[]{}^| + return false + } + } + + return true +} + +// ToRFC1459 converts a string to the stripped down conversion within RFC +// 1459. This will do things like replace an "A" with an "a", "[]" with "{}", +// and so forth. Useful to compare two nicknames or channels. Note that this +// should not be used to normalize nicknames or similar, as this may convert +// valid input characters to non-rfc-valid characters. As such, it's main use +// is for comparing two nicks. +func ToRFC1459(input string) string { + var out string + + for i := 0; i < len(input); i++ { + if input[i] >= 65 && input[i] <= 94 { + out += string(rune(input[i]) + 32) + } else { + out += string(input[i]) + } + } + + return out +} + +const globChar = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// string. The glob character is *. +func Glob(input, match string) bool { + // Empty pattern. + if match == "" { + return input == match + } + + // If a glob, match all. + if match == globChar { + return true + } + + parts := strings.Split(match, globChar) + + if len(parts) == 1 { + // No globs, test for equality. + return input == match + } + + leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar) + last := len(parts) - 1 + + // Check prefix first. + if !leadingGlob && !strings.HasPrefix(input, parts[0]) { + return false + } + + // Check middle section. + for i := 1; i < last; i++ { + if !strings.Contains(input, parts[i]) { + return false + } + + // Trim already-evaluated text from input during loop over match + // text. + idx := strings.Index(input, parts[i]) + len(parts[i]) + input = input[idx:] + } + + // Check suffix last. + return trailingGlob || strings.HasSuffix(input, parts[last]) +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/handler.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/handler.go new file mode 100644 index 0000000..55fd4e9 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/handler.go @@ -0,0 +1,506 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "fmt" + "log" + "math/rand" + "runtime" + "runtime/debug" + "strings" + "sync" + "time" +) + +// RunHandlers manually runs handlers for a given event. +func (c *Client) RunHandlers(event *Event) { + if event == nil { + return + } + + // Log the event. + prefix := "< " + if event.Echo { + prefix += "[echo-message] " + } + c.debug.Print(prefix + StripRaw(event.String())) + if c.Config.Out != nil { + if pretty, ok := event.Pretty(); ok { + fmt.Fprintln(c.Config.Out, StripRaw(pretty)) + } + } + + // Background handlers first. If the event is an echo-message, then only + // send the echo version to ALL_EVENTS. + c.Handlers.exec(ALL_EVENTS, true, c, event.Copy()) + if !event.Echo { + c.Handlers.exec(event.Command, true, c, event.Copy()) + } + + c.Handlers.exec(ALL_EVENTS, false, c, event.Copy()) + if !event.Echo { + c.Handlers.exec(event.Command, false, c, event.Copy()) + } + + // Check if it's a CTCP. + if ctcp := DecodeCTCP(event.Copy()); ctcp != nil { + // Execute it. + c.CTCP.call(c, ctcp) + } +} + +// Handler is lower level implementation of a handler. See +// Caller.AddHandler() +type Handler interface { + Execute(*Client, Event) +} + +// HandlerFunc is a type that represents the function necessary to +// implement Handler. +type HandlerFunc func(client *Client, event Event) + +// Execute calls the HandlerFunc with the sender and irc message. +func (f HandlerFunc) Execute(client *Client, event Event) { + f(client, event) +} + +// Caller manages internal and external (user facing) handlers. +type Caller struct { + // mu is the mutex that should be used when accessing handlers. + mu sync.RWMutex + + // external/internal keys are of structure: + // map[COMMAND][CUID]Handler + // Also of note: "COMMAND" should always be uppercase for normalization. + + // external is a map of user facing handlers. + external map[string]map[string]Handler + // internal is a map of internally used handlers for the client. + internal map[string]map[string]Handler + // debug is the clients logger used for debugging. + debug *log.Logger +} + +// newCaller creates and initializes a new handler. +func newCaller(debugOut *log.Logger) *Caller { + c := &Caller{ + external: map[string]map[string]Handler{}, + internal: map[string]map[string]Handler{}, + debug: debugOut, + } + + return c +} + +// Len returns the total amount of user-entered registered handlers. +func (c *Caller) Len() int { + var total int + + c.mu.RLock() + for command := range c.external { + total += len(c.external[command]) + } + c.mu.RUnlock() + + return total +} + +// Count is much like Caller.Len(), however it counts the number of +// registered handlers for a given command. +func (c *Caller) Count(cmd string) int { + var total int + + cmd = strings.ToUpper(cmd) + + c.mu.RLock() + for command := range c.external { + if command == cmd { + total += len(c.external[command]) + } + } + c.mu.RUnlock() + + return total +} + +func (c *Caller) String() string { + var total int + + c.mu.RLock() + for cmd := range c.internal { + total += len(c.internal[cmd]) + } + c.mu.RUnlock() + + return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), total) +} + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// cuid generates a unique UID string for each handler for ease of removal. +func (c *Caller) cuid(cmd string, n int) (cuid, uid string) { + b := make([]byte, n) + + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + + return cmd + ":" + string(b), string(b) +} + +// cuidToID allows easy mapping between a generated cuid and the caller +// external/internal handler maps. +func (c *Caller) cuidToID(input string) (cmd, uid string) { + i := strings.IndexByte(input, ':') + if i < 0 { + return "", "" + } + + return input[:i], input[i+1:] +} + +type execStack struct { + Handler + cuid string +} + +// exec executes all handlers pertaining to specified event. Internal first, +// then external. +// +// Please note that there is no specific order/priority for which the handlers +// are executed. +func (c *Caller) exec(command string, bg bool, client *Client, event *Event) { + // Build a stack of handlers which can be executed concurrently. + var stack []execStack + + c.mu.RLock() + // Get internal handlers first. + if _, ok := c.internal[command]; ok { + for cuid := range c.internal[command] { + if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) { + continue + } + + stack = append(stack, execStack{c.internal[command][cuid], cuid}) + } + } + + // Then external handlers. + if _, ok := c.external[command]; ok { + for cuid := range c.external[command] { + if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) { + continue + } + + stack = append(stack, execStack{c.external[command][cuid], cuid}) + } + } + c.mu.RUnlock() + + // Run all handlers concurrently across the same event. This should + // still help prevent mis-ordered events, while speeding up the + // execution speed. + var wg sync.WaitGroup + wg.Add(len(stack)) + for i := 0; i < len(stack); i++ { + go func(index int) { + defer wg.Done() + c.debug.Printf("[%d/%d] exec %s => %s", index+1, len(stack), stack[index].cuid, command) + start := time.Now() + + if bg { + go func() { + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, event, stack[index].cuid, 3) + } + + stack[index].Execute(client, *event) + c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start)) + }() + + return + } + + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, event, stack[index].cuid, 3) + } + + stack[index].Execute(client, *event) + c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start)) + }(i) + } + + // Wait for all of the handlers to complete. Not doing this may cause + // new events from becoming ahead of older handlers. + wg.Wait() +} + +// ClearAll clears all external handlers currently setup within the client. +// This ignores internal handlers. +func (c *Caller) ClearAll() { + c.mu.Lock() + c.external = map[string]map[string]Handler{} + c.mu.Unlock() + + c.debug.Print("cleared all external handlers") +} + +// clearInternal clears all internal handlers currently setup within the +// client. +func (c *Caller) clearInternal() { + c.mu.Lock() + c.internal = map[string]map[string]Handler{} + c.mu.Unlock() + + c.debug.Print("cleared all internal handlers") +} + +// Clear clears all of the handlers for the given event. +// This ignores internal handlers. +func (c *Caller) Clear(cmd string) { + cmd = strings.ToUpper(cmd) + + c.mu.Lock() + if _, ok := c.external[cmd]; ok { + delete(c.external, cmd) + } + c.mu.Unlock() + + c.debug.Printf("cleared external handlers for %s", cmd) +} + +// Remove removes the handler with cuid from the handler stack. success +// indicates that it existed, and has been removed. If not success, it +// wasn't a registered handler. +func (c *Caller) Remove(cuid string) (success bool) { + c.mu.Lock() + success = c.remove(cuid) + c.mu.Unlock() + + return success +} + +// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu +// on your own. +func (c *Caller) remove(cuid string) (success bool) { + cmd, uid := c.cuidToID(cuid) + if cmd == "" || uid == "" { + return false + } + + // Check if the irc command/event has any handlers on it. + if _, ok := c.external[cmd]; !ok { + return false + } + + // Check to see if it's actually a registered handler. + if _, ok := c.external[cmd][uid]; !ok { + return false + } + + delete(c.external[cmd], uid) + c.debug.Printf("removed handler %s", cuid) + + // Assume success. + return true +} + +// sregister is much like Caller.register(), except that it safely locks +// the Caller mutex. +func (c *Caller) sregister(internal, bg bool, cmd string, handler Handler) (cuid string) { + c.mu.Lock() + cuid = c.register(internal, bg, cmd, handler) + c.mu.Unlock() + + return cuid +} + +// register will register a handler in the internal tracker. Unsafe (you +// must lock c.mu yourself!) +func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid string) { + var uid string + + cmd = strings.ToUpper(cmd) + + cuid, uid = c.cuid(cmd, 20) + if bg { + uid += ":bg" + cuid += ":bg" + } + + if internal { + if _, ok := c.internal[cmd]; !ok { + c.internal[cmd] = map[string]Handler{} + } + + c.internal[cmd][uid] = handler + } else { + if _, ok := c.external[cmd]; !ok { + c.external[cmd] = map[string]Handler{} + } + + c.external[cmd][uid] = handler + } + + _, file, line, _ := runtime.Caller(3) + + c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line) + + return cuid +} + +// AddHandler registers a handler (matching the handler interface) for the +// given event. cuid is the handler uid which can be used to remove the +// handler with Caller.Remove(). +func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) { + return c.sregister(false, false, cmd, handler) +} + +// Add registers the handler function for the given event. cuid is the +// handler uid which can be used to remove the handler with Caller.Remove(). +func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) { + return c.sregister(false, false, cmd, HandlerFunc(handler)) +} + +// AddBg registers the handler function for the given event and executes it +// in a go-routine. cuid is the handler uid which can be used to remove the +// handler with Caller.Remove(). +func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) { + return c.sregister(false, true, cmd, HandlerFunc(handler)) +} + +// AddTmp adds a "temporary" handler, which is good for one-time or few-time +// uses. This supports a deadline and/or manual removal, as this differs +// much from how normal handlers work. An example of a good use for this +// would be to capture the entire output of a multi-response query to the +// server. (e.g. LIST, WHOIS, etc) +// +// The supplied handler is able to return a boolean, which if true, will +// remove the handler from the handler stack. +// +// Additionally, AddTmp has a useful option, deadline. When set to greater +// than 0, deadline will be the amount of time that passes before the handler +// is removed from the stack, regardless of if the handler returns true or not. +// This is useful in that it ensures that the handler is cleaned up if the +// server does not respond appropriately, or takes too long to respond. +// +// Note that handlers supplied with AddTmp are executed in a goroutine to +// ensure that they are not blocking other handlers. However, if you are +// creating a temporary handler from another handler, it should be a +// background handler. +// +// Use cuid with Caller.Remove() to prematurely remove the handler from the +// stack, bypassing the timeout or waiting for the handler to return that it +// wants to be removed from the stack. +func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) { + done = make(chan struct{}) + + cuid = c.sregister(false, true, cmd, HandlerFunc(func(client *Client, event Event) { + remove := handler(client, event) + if remove { + if ok := c.Remove(cuid); ok { + close(done) + } + } + })) + + if deadline > 0 { + go func() { + select { + case <-time.After(deadline): + case <-done: + } + + if ok := c.Remove(cuid); ok { + close(done) + } + }() + } + + return cuid, done +} + +// recoverHandlerPanic is used to catch all handler panics, and re-route +// them if necessary. +func recoverHandlerPanic(client *Client, event *Event, id string, skip int) { + perr := recover() + if perr == nil { + return + } + + var file, function string + var line int + var ok bool + + var pcs [10]uintptr + frames := runtime.CallersFrames(pcs[:runtime.Callers(skip, pcs[:])]) + for { + frame, _ := frames.Next() + file = frame.File + line = frame.Line + function = frame.Function + + break + } + + err := &HandlerError{ + Event: *event, + ID: id, + File: file, + Line: line, + Func: function, + Panic: perr, + Stack: debug.Stack(), + callOk: ok, + } + + client.Config.RecoverFunc(client, err) +} + +// HandlerError is the error returned when a panic is intentionally recovered +// from. It contains useful information like the handler identifier (if +// applicable), filename, line in file where panic occurred, the call +// trace, and original event. +type HandlerError struct { + Event Event // Event is the event that caused the error. + ID string // ID is the CUID of the handler. + File string // File is the file from where the panic originated. + Line int // Line number where panic originated. + Func string // Function name where panic originated. + Panic interface{} // Panic is the error that was passed to panic(). + Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions. + callOk bool +} + +// Error returns a prettified version of HandlerError, containing ID, file, +// line, and basic error string. +func (e *HandlerError) Error() string { + if e.callOk { + return fmt.Sprintf("panic during handler [%s] execution in %s:%d: %s", e.ID, e.File, e.Line, e.Panic) + } + + return fmt.Sprintf("panic during handler [%s] execution in unknown: %s", e.ID, e.Panic) +} + +// String returns the error that panic returned, as well as the entire call +// trace of where it originated. +func (e *HandlerError) String() string { + return fmt.Sprintf("panic: %s\n\n%s", e.Panic, string(e.Stack)) +} + +// DefaultRecoverHandler can be used with Config.RecoverFunc as a default +// catch-all for panics. This will log the error, and the call trace to the +// debug log (see Config.Debug), or os.Stdout if Config.Debug is unset. +func DefaultRecoverHandler(client *Client, err *HandlerError) { + if client.Config.Debug == nil { + fmt.Println(err.Error()) + fmt.Println(err.String()) + return + } + + client.debug.Println(err.Error()) + client.debug.Println(err.String()) +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/modes.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/modes.go new file mode 100644 index 0000000..35ff103 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/modes.go @@ -0,0 +1,550 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "encoding/json" + "strings" + "sync" +) + +// CMode represents a single step of a given mode change. +type CMode struct { + add bool // if it's a +, or -. + name byte // character representation of the given mode. + setting bool // if it's a setting (should be stored) or temporary (op/voice/etc). + args string // arguments to the mode, if arguments are supported. +} + +// Short returns a short representation of a mode without arguments. E.g. "+a", +// or "-b". +func (c *CMode) Short() string { + var status string + if c.add { + status = "+" + } else { + status = "-" + } + + return status + string(c.name) +} + +// String returns a string representation of a mode, including optional +// arguments. E.g. "+b user*!ident@host.*.com" +func (c *CMode) String() string { + if c.args == "" { + return c.Short() + } + + return c.Short() + " " + c.args +} + +// CModes is a representation of a set of modes. This may be the given state +// of a channel/user, or the given state changes to a given channel/user. +type CModes struct { + raw string // raw supported modes. + modesListArgs string // modes that add/remove users from lists and support args. + modesArgs string // modes that support args. + modesSetArgs string // modes that support args ONLY when set. + modesNoArgs string // modes that do not support args. + + prefixes string // user permission prefixes. these aren't a CMode.setting. + modes []CMode // the list of modes for this given state. +} + +// Copy returns a deep copy of CModes. +func (c *CModes) Copy() (nc CModes) { + nc = CModes{} + nc = *c + + nc.modes = make([]CMode, len(c.modes)) + + // Copy modes. + for i := 0; i < len(c.modes); i++ { + nc.modes[i] = c.modes[i] + } + + return nc +} + +// String returns a complete set of modes for this given state (change?). For +// example, "+a-b+cde some-arg". +func (c *CModes) String() string { + var out string + var args string + + if len(c.modes) > 0 { + out += "+" + } + + for i := 0; i < len(c.modes); i++ { + out += string(c.modes[i].name) + + if len(c.modes[i].args) > 0 { + args += " " + c.modes[i].args + } + } + + return out + args +} + +// HasMode checks if the CModes state has a given mode. E.g. "m", or "I". +func (c *CModes) HasMode(mode string) bool { + for i := 0; i < len(c.modes); i++ { + if string(c.modes[i].name) == mode { + return true + } + } + + return false +} + +// Get returns the arguments for a given mode within this session, if it +// supports args. +func (c *CModes) Get(mode string) (args string, ok bool) { + for i := 0; i < len(c.modes); i++ { + if string(c.modes[i].name) == mode { + if c.modes[i].args == "" { + return "", false + } + + return c.modes[i].args, true + } + } + + return "", false +} + +// hasArg checks to see if the mode supports arguments. What ones support this?: +// A = Mode that adds or removes a nick or address to a list. Always has a parameter. +// B = Mode that changes a setting and always has a parameter. +// C = Mode that changes a setting and only has a parameter when set. +// D = Mode that changes a setting and never has a parameter. +// Note: Modes of type A return the list when there is no parameter present. +// Note: Some clients assumes that any mode not listed is of type D. +// Note: Modes in PREFIX are not listed but could be considered type B. +func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) { + if len(c.raw) < 1 { + return false, true + } + + if strings.IndexByte(c.modesListArgs, mode) > -1 { + return true, false + } + + if strings.IndexByte(c.modesArgs, mode) > -1 { + return true, true + } + + if strings.IndexByte(c.modesSetArgs, mode) > -1 { + if set { + return true, true + } + + return false, true + } + + if strings.IndexByte(c.prefixes, mode) > -1 { + return true, false + } + + return false, true +} + +// Apply merges two state changes, or one state change into a state of modes. +// For example, the latter would mean applying an incoming MODE with the modes +// stored for a channel. +func (c *CModes) Apply(modes []CMode) { + var newModes []CMode + + for j := 0; j < len(c.modes); j++ { + isin := false + for i := 0; i < len(modes); i++ { + if !modes[i].setting { + continue + } + if c.modes[j].name == modes[i].name && modes[i].add { + newModes = append(newModes, modes[i]) + isin = true + break + } + } + + if !isin { + newModes = append(newModes, c.modes[j]) + } + } + + for i := 0; i < len(modes); i++ { + if !modes[i].setting || !modes[i].add { + continue + } + + isin := false + for j := 0; j < len(newModes); j++ { + if modes[i].name == newModes[j].name { + isin = true + break + } + } + + if !isin { + newModes = append(newModes, modes[i]) + } + } + + c.modes = newModes +} + +// Parse parses a set of flags and args, returning the necessary list of +// mappings for the mode flags. +func (c *CModes) Parse(flags string, args []string) (out []CMode) { + // add is the mode state we're currently in. Adding, or removing modes. + add := true + var argCount int + + for i := 0; i < len(flags); i++ { + if flags[i] == '+' { + add = true + continue + } + if flags[i] == '-' { + add = false + continue + } + + mode := CMode{ + name: flags[i], + add: add, + } + + hasArgs, isSetting := c.hasArg(add, flags[i]) + if hasArgs && len(args) > argCount { + mode.args = args[argCount] + argCount++ + } + mode.setting = isSetting + + out = append(out, mode) + } + + return out +} + +// NewCModes returns a new CModes reference. channelModes and userPrefixes +// would be something you see from the server's "CHANMODES" and "PREFIX" +// ISUPPORT capability messages (alternatively, fall back to the standard) +// DefaultPrefixes and ModeDefaults. +func NewCModes(channelModes, userPrefixes string) CModes { + split := strings.SplitN(channelModes, ",", 4) + if len(split) != 4 { + for i := len(split); i < 4; i++ { + split = append(split, "") + } + } + + return CModes{ + raw: channelModes, + modesListArgs: split[0], + modesArgs: split[1], + modesSetArgs: split[2], + modesNoArgs: split[3], + + prefixes: userPrefixes, + modes: []CMode{}, + } +} + +// IsValidChannelMode validates a channel mode (CHANMODES). +func IsValidChannelMode(raw string) bool { + if len(raw) < 1 { + return false + } + + for i := 0; i < len(raw); i++ { + // Allowed are: ",", A-Z and a-z. + if raw[i] != ',' && (raw[i] < 'A' || raw[i] > 'Z') && (raw[i] < 'a' || raw[i] > 'z') { + return false + } + } + + return true +} + +// isValidUserPrefix validates a list of ISUPPORT-style user prefixes (PREFIX). +func isValidUserPrefix(raw string) bool { + if len(raw) < 1 { + return false + } + + if raw[0] != '(' { + return false + } + + var keys, rep int + var passedKeys bool + + // Skip the first one as we know it's (. + for i := 1; i < len(raw); i++ { + if raw[i] == ')' { + passedKeys = true + continue + } + + if passedKeys { + rep++ + } else { + keys++ + } + } + + return keys == rep +} + +// parsePrefixes parses the mode character mappings from the symbols of a +// ISUPPORT-style user prefixes list (PREFIX). +func parsePrefixes(raw string) (modes, prefixes string) { + if !isValidUserPrefix(raw) { + return modes, prefixes + } + + i := strings.Index(raw, ")") + if i < 1 { + return modes, prefixes + } + + return raw[1:i], raw[i+1:] +} + +// handleMODE handles incoming MODE messages, and updates the tracking +// information for each channel, as well as if any of the modes affect user +// permissions. +func handleMODE(c *Client, e Event) { + // Check if it's a RPL_CHANNELMODEIS. + if e.Command == RPL_CHANNELMODEIS && len(e.Params) > 2 { + // RPL_CHANNELMODEIS sends the user as the first param, skip it. + e.Params = e.Params[1:] + } + // Should be at least MODE <target> <flags>, to be useful. As well, only + // tracking channel modes at the moment. + if len(e.Params) < 2 || !IsValidChannel(e.Params[0]) { + return + } + + c.state.RLock() + channel := c.state.lookupChannel(e.Params[0]) + if channel == nil { + c.state.RUnlock() + return + } + + flags := e.Params[1] + var args []string + if len(e.Params) > 2 { + args = append(args, e.Params[2:]...) + } + + modes := channel.Modes.Parse(flags, args) + channel.Modes.Apply(modes) + + // Loop through and update users modes as necessary. + for i := 0; i < len(modes); i++ { + if modes[i].setting || modes[i].args == "" { + continue + } + + user := c.state.lookupUser(modes[i].args) + if user != nil { + perms, _ := user.Perms.Lookup(channel.Name) + perms.setFromMode(modes[i]) + user.Perms.set(channel.Name, perms) + } + } + + c.state.RUnlock() + c.state.notify(c, UPDATE_STATE) +} + +// chanModes returns the ISUPPORT list of server-supported channel modes, +// alternatively falling back to ModeDefaults. +func (s *state) chanModes() string { + if modes, ok := s.serverOptions["CHANMODES"]; ok && IsValidChannelMode(modes) { + return modes + } + + return ModeDefaults +} + +// userPrefixes returns the ISUPPORT list of server-supported user prefixes. +// This includes mode characters, as well as user prefix symbols. Falls back +// to DefaultPrefixes if not server-supported. +func (s *state) userPrefixes() string { + if prefix, ok := s.serverOptions["PREFIX"]; ok && isValidUserPrefix(prefix) { + return prefix + } + + return DefaultPrefixes +} + +// UserPerms contains all of the permissions for each channel the user is +// in. +type UserPerms struct { + mu sync.RWMutex + channels map[string]Perms +} + +// Copy returns a deep copy of the channel permissions. +func (p *UserPerms) Copy() (perms *UserPerms) { + np := &UserPerms{ + channels: make(map[string]Perms), + } + + p.mu.RLock() + for key := range p.channels { + np.channels[key] = p.channels[key] + } + p.mu.RUnlock() + + return np +} + +// MarshalJSON implements json.Marshaler. +func (p *UserPerms) MarshalJSON() ([]byte, error) { + p.mu.Lock() + out, err := json.Marshal(&p.channels) + p.mu.Unlock() + + return out, err +} + +// Lookup looks up the users permissions for a given channel. ok is false +// if the user is not in the given channel. +func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) { + p.mu.RLock() + perms, ok = p.channels[ToRFC1459(channel)] + p.mu.RUnlock() + + return perms, ok +} + +func (p *UserPerms) set(channel string, perms Perms) { + p.mu.Lock() + p.channels[ToRFC1459(channel)] = perms + p.mu.Unlock() +} + +func (p *UserPerms) remove(channel string) { + p.mu.Lock() + delete(p.channels, ToRFC1459(channel)) + p.mu.Unlock() +} + +// Perms contains all channel-based user permissions. The minimum op, and +// voice should be supported on all networks. This also supports non-rfc +// Owner, Admin, and HalfOp, if the network has support for it. +type Perms struct { + // Owner (non-rfc) indicates that the user has full permissions to the + // channel. More than one user can have owner permission. + Owner bool `json:"owner"` + // Admin (non-rfc) is commonly given to users that are trusted enough + // to manage channel permissions, as well as higher level service settings. + Admin bool `json:"admin"` + // Op is commonly given to trusted users who can manage a given channel + // by kicking, and banning users. + Op bool `json:"op"` + // HalfOp (non-rfc) is commonly used to give users permissions like the + // ability to kick, without giving them greater abilities to ban all users. + HalfOp bool `json:"half_op"` + // Voice indicates the user has voice permissions, commonly given to known + // users, with very light trust, or to indicate a user is active. + Voice bool `json:"voice"` +} + +// IsAdmin indicates that the user has banning abilities, and are likely a +// very trustable user (e.g. op+). +func (m Perms) IsAdmin() bool { + if m.Owner || m.Admin || m.Op { + return true + } + + return false +} + +// IsTrusted indicates that the user at least has modes set upon them, higher +// than a regular joining user. +func (m Perms) IsTrusted() bool { + if m.IsAdmin() || m.HalfOp || m.Voice { + return true + } + + return false +} + +// reset resets the modes of a user. +func (m *Perms) reset() { + m.Owner = false + m.Admin = false + m.Op = false + m.HalfOp = false + m.Voice = false +} + +// set translates raw prefix characters into proper permissions. Only +// use this function when you have a session lock. +func (m *Perms) set(prefix string, add bool) { + if !add { + m.reset() + } + + for i := 0; i < len(prefix); i++ { + switch string(prefix[i]) { + case OwnerPrefix: + m.Owner = true + case AdminPrefix: + m.Admin = true + case OperatorPrefix: + m.Op = true + case HalfOperatorPrefix: + m.HalfOp = true + case VoicePrefix: + m.Voice = true + } + } +} + +// setFromMode sets user-permissions based on channel user mode chars. E.g. +// "o" being oper, "v" being voice, etc. +func (m *Perms) setFromMode(mode CMode) { + switch string(mode.name) { + case ModeOwner: + m.Owner = mode.add + case ModeAdmin: + m.Admin = mode.add + case ModeOperator: + m.Op = mode.add + case ModeHalfOperator: + m.HalfOp = mode.add + case ModeVoice: + m.Voice = mode.add + } +} + +// parseUserPrefix parses a raw mode line, like "@user" or "@+user". +func parseUserPrefix(raw string) (modes, nick string, success bool) { + for i := 0; i < len(raw); i++ { + char := string(raw[i]) + + if char == OwnerPrefix || char == AdminPrefix || char == HalfOperatorPrefix || + char == OperatorPrefix || char == VoicePrefix { + modes += char + continue + } + + // Assume we've gotten to the nickname part. + return modes, raw[i:], true + } + + return +} diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/state.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/state.go new file mode 100644 index 0000000..d9e7298 --- /dev/null +++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/state.go @@ -0,0 +1,550 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "fmt" + "sort" + "sync" + "time" +) + +// state represents the actively-changing variables within the client +// runtime. Note that everything within the state should be guarded by the +// embedded sync.RWMutex. +type state struct { + sync.RWMutex + // nick, ident, and host are the internal trackers for our user. + nick, ident, host string + // channels represents all channels we're active in. + channels map[string]*Channel + // users represents all of users that we're tracking. + users map[string]*User + // enabledCap are the capabilities which are enabled for this connection. + enabledCap map[string]map[string]string + // tmpCap are the capabilties which we share with the server during the + // last capability check. These will get sent once we have received the + // last capability list command from the server. + tmpCap map[string]map[string]string + // serverOptions are the standard capabilities and configurations + // supported by the server at connection time. This also includes + // RPL_ISUPPORT entries. + serverOptions map[string]string + // motd is the servers message of the day. + motd string + + // sts are strict transport security configurations, if specified by the + // server. + // + // TODO: ideally, this would be a configurable policy store that the user could + // optionally override (to store STS information on disk, memory, etc). + sts strictTransport +} + +// reset resets the state back to it's original form. +func (s *state) reset(initial bool) { + s.Lock() + s.nick = "" + s.ident = "" + s.host = "" + s.channels = make(map[string]*Channel) + s.users = make(map[string]*User) + s.serverOptions = make(map[string]string) + s.enabledCap = make(map[string]map[string]string) + s.tmpCap = make(map[string]map[string]string) + s.motd = "" + + if initial { + s.sts.reset() + } + s.Unlock() +} + +// User represents an IRC user and the state attached to them. +type User struct { + // Nick is the users current nickname. rfc1459 compliant. + Nick string `json:"nick"` + // Ident is the users username/ident. Ident is commonly prefixed with a + // "~", which indicates that they do not have a identd server setup for + // authentication. + Ident string `json:"ident"` + // Host is the visible host of the users connection that the server has + // provided to us for their connection. May not always be accurate due to + // many networks spoofing/hiding parts of the hostname for privacy + // reasons. + Host string `json:"host"` + + // ChannelList is a sorted list of all channels that we are currently + // tracking the user in. Each channel name is rfc1459 compliant. See + // User.Channels() for a shorthand if you're looking for the *Channel + // version of the channel list. + ChannelList []string `json:"channels"` + + // FirstSeen represents the first time that the user was seen by the + // client for the given channel. Only usable if from state, not in past. + FirstSeen time.Time `json:"first_seen"` + // LastActive represents the last time that we saw the user active, + // which could be during nickname change, message, channel join, etc. + // Only usable if from state, not in past. + LastActive time.Time `json:"last_active"` + + // Perms are the user permissions applied to this user that affect the given + // channel. This supports non-rfc style modes like Admin, Owner, and HalfOp. + Perms *UserPerms `json:"perms"` + + // Extras are things added on by additional tracking methods, which may + // or may not work on the IRC server in mention. + Extras struct { + // Name is the users "realname" or full name. Commonly contains links + // to the IRC client being used, or something of non-importance. May + // also be empty if unsupported by the server/tracking is disabled. + Name string `json:"name"` + // Account refers to the account which the user is authenticated as. + // This differs between each network (e.g. usually Nickserv, but + // could also be something like Undernet). May also be empty if + // unsupported by the server/tracking is disabled. + Account string `json:"account"` + // Away refers to the away status of the user. An empty string + // indicates that they are active, otherwise the string is what they + // set as their away message. May also be empty if unsupported by the + // server/tracking is disabled. + Away string `json:"away"` + } `json:"extras"` +} + +// Channels returns a reference of *Channels that the client knows the user +// is in. If you're just looking for the namme of the channels, use +// User.ChannelList. +func (u User) Channels(c *Client) []*Channel { + if c == nil { + panic("nil Client provided") + } + + channels := []*Channel{} + + c.state.RLock() + for i := 0; i < len(u.ChannelList); i++ { + ch := c.state.lookupChannel(u.ChannelList[i]) + if ch != nil { + channels = append(channels, ch) + } + } + c.state.RUnlock() + + return channels +} + +// Copy returns a deep copy of the user which can be modified without making +// changes to the actual state. +func (u *User) Copy() *User { + if u == nil { + return nil + } + + nu := &User{} + *nu = *u + + nu.Perms = u.Perms.Copy() + _ = copy(nu.ChannelList, u.ChannelList) + + return nu +} + +// addChannel adds the channel to the users channel list. +func (u *User) addChannel(name string) { + if u.InChannel(name) { + return + } + + u.ChannelList = append(u.ChannelList, ToRFC1459(name)) + sort.Strings(u.ChannelList) + + u.Perms.set(name, Perms{}) +} + +// deleteChannel removes an existing channel from the users channel list. +func (u *User) deleteChannel(name string) { + name = ToRFC1459(name) + + j := -1 + for i := 0; i < len(u.ChannelList); i++ { + if u.ChannelList[i] == name { + j = i + break + } + } + + if j != -1 { + u.ChannelList = append(u.ChannelList[:j], u.ChannelList[j+1:]...) + } + + u.Perms.remove(name) +} + +// InChannel checks to see if a user is in the given channel. +func (u *User) InChannel(name string) bool { + name = ToRFC1459(name) + + for i := 0; i < len(u.ChannelList); i++ { + if u.ChannelList[i] == name { + return true + } + } + + return false +} + +// Lifetime represents the amount of time that has passed since we have first +// seen the user. +func (u *User) Lifetime() time.Duration { + return time.Since(u.FirstSeen) +} + +// Active represents the the amount of time that has passed since we have +// last seen the user. +func (u *User) Active() time.Duration { + return time.Since(u.LastActive) +} + +// IsActive returns true if they were active within the last 30 minutes. +func (u *User) IsActive() bool { + return u.Active() < (time.Minute * 30) +} + +// Channel represents an IRC channel and the state attached to it. +type Channel struct { + // Name of the channel. Must be rfc1459 compliant. + Name string `json:"name"` + // Topic of the channel. + Topic string `json:"topic"` + + // UserList is a sorted list of all users we are currently tracking within + // the channel. Each is the nickname, and is rfc1459 compliant. + UserList []string `json:"user_list"` + // Joined represents the first time that the client joined the channel. + Joined time.Time `json:"joined"` + // Modes are the known channel modes that the bot has captured. + Modes CModes `json:"modes"` +} + +// Users returns a reference of *Users that the client knows the channel has +// If you're just looking for just the name of the users, use Channnel.UserList. +func (ch Channel) Users(c *Client) []*User { + if c == nil { + panic("nil Client provided") + } + + users := []*User{} + + c.state.RLock() + for i := 0; i < len(ch.UserList); i++ { + user := c.state.lookupUser(ch.UserList[i]) + if user != nil { + users = append(users, user) + } + } + c.state.RUnlock() + + return users +} + +// Trusted returns a list of users which have voice or greater in the given +// channel. See Perms.IsTrusted() for more information. +func (ch Channel) Trusted(c *Client) []*User { + if c == nil { + panic("nil Client provided") + } + + users := []*User{} + + c.state.RLock() + for i := 0; i < len(ch.UserList); i++ { + user := c.state.lookupUser(ch.UserList[i]) + if user == nil { + continue + } + + perms, ok := user.Perms.Lookup(ch.Name) + if ok && perms.IsTrusted() { + users = append(users, user) + } + } + c.state.RUnlock() + + return users +} + +// Admins returns a list of users which have half-op (if supported), or +// greater permissions (op, admin, owner, etc) in the given channel. See +// Perms.IsAdmin() for more information. +func (ch Channel) Admins(c *Client) []*User { + if c == nil { + panic("nil Client provided") + } + + users := []*User{} + + c.state.RLock() + for i := 0; i < len(ch.UserList); i++ { + user := c.state.lookupUser(ch.UserList[i]) + if user == nil { + continue + } + + perms, ok := user.Perms.Lookup(ch.Name) + if ok && perms.IsAdmin() { + users = append(users, user) + } + } + c.state.RUnlock() + + return users +} + +// addUser adds a user to the users list. +func (ch *Channel) addUser(nick string) { + if ch.UserIn(nick) { + return + } + + ch.UserList = append(ch.UserList, ToRFC1459(nick)) + sort.Strings(ch.UserList) +} + +// deleteUser removes an existing user from the users list. +func (ch *Channel) deleteUser(nick string) { + nick = ToRFC1459(nick) + + j := -1 + for i := 0; i < len(ch.UserList); i++ { + if ch.UserList[i] == nick { + j = i + break + } + } + + if j != -1 { + ch.UserList = append(ch.UserList[:j], ch.UserList[j+1:]...) + } +} + +// Copy returns a deep copy of a given channel. +func (ch *Channel) Copy() *Channel { + if ch == nil { + return nil + } + + nc := &Channel{} + *nc = *ch + + _ = copy(nc.UserList, ch.UserList) + + // And modes. + nc.Modes = ch.Modes.Copy() + + return nc +} + +// Len returns the count of users in a given channel. +func (ch *Channel) Len() int { + return len(ch.UserList) +} + +// UserIn checks to see if a given user is in a channel. +func (ch *Channel) UserIn(name string) bool { + name = ToRFC1459(name) + + for i := 0; i < len(ch.UserList); i++ { + if ch.UserList[i] == name { + return true + } + } + + return false +} + +// Lifetime represents the amount of time that has passed since we have first +// joined the channel. +func (ch *Channel) Lifetime() time.Duration { + return time.Since(ch.Joined) +} + +// createChannel creates the channel in state, if not already done. +func (s *state) createChannel(name string) (ok bool) { + supported := s.chanModes() + prefixes, _ := parsePrefixes(s.userPrefixes()) + + if _, ok := s.channels[ToRFC1459(name)]; ok { + return false + } + + s.channels[ToRFC1459(name)] = &Channel{ + Name: name, + UserList: []string{}, + Joined: time.Now(), + Modes: NewCModes(supported, prefixes), + } + + return true +} + +// deleteChannel removes the channel from state, if not already done. +func (s *state) deleteChannel(name string) { + name = ToRFC1459(name) + + _, ok := s.channels[name] + if !ok { + return + } + + for _, user := range s.channels[name].UserList { + s.users[user].deleteChannel(name) + + if len(s.users[user].ChannelList) == 0 { + // Assume we were only tracking them in this channel, and they + // should be removed from state. + + delete(s.users, user) + } + } + + delete(s.channels, name) +} + +// lookupChannel returns a reference to a channel, nil returned if no results +// found. +func (s *state) lookupChannel(name string) *Channel { + return s.channels[ToRFC1459(name)] +} + +// lookupUser returns a reference to a user, nil returned if no results +// found. +func (s *state) lookupUser(name string) *User { + return s.users[ToRFC1459(name)] +} + +// createUser creates the user in state, if not already done. +func (s *state) createUser(src *Source) (ok bool) { + if _, ok := s.users[src.ID()]; ok { + // User already exists. + return false + } + + s.users[src.ID()] = &User{ + Nick: src.Name, + Host: src.Host, + Ident: src.Ident, + FirstSeen: time.Now(), + LastActive: time.Now(), + Perms: &UserPerms{channels: make(map[string]Perms)}, + } + + return true +} + +// deleteUser removes the user from channel state. +func (s *state) deleteUser(channelName, nick string) { + user := s.lookupUser(nick) + if user == nil { + return + } + + if channelName == "" { + for i := 0; i < len(user.ChannelList); i++ { + s.channels[user.ChannelList[i]].deleteUser(nick) + } + + delete(s.users, ToRFC1459(nick)) + return + } + + channel := s.lookupChannel(channelName) + if channel == nil { + return + } + + user.deleteChannel(channelName) + channel.deleteUser(nick) + + if len(user.ChannelList) == 0 { + // This means they are no longer in any channels we track, delete + // them from state. + + delete(s.users, ToRFC1459(nick)) + } +} + +// renameUser renames the user in state, in all locations where relevant. +func (s *state) renameUser(from, to string) { + from = ToRFC1459(from) + + // Update our nickname. + if from == ToRFC1459(s.nick) { + s.nick = to + } + + user := s.lookupUser(from) + if user == nil { + return + } + + delete(s.users, from) + + user.Nick = to + user.LastActive = time.Now() + s.users[ToRFC1459(to)] = user + + for i := 0; i < len(user.ChannelList); i++ { + for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ { + if s.channels[user.ChannelList[i]].UserList[j] == from { + s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to) + + sort.Strings(s.channels[user.ChannelList[i]].UserList) + break + } + } + } +} + +type strictTransport struct { + beginUpgrade bool + upgradePort int + persistenceDuration int + persistenceReceived time.Time + preload bool + lastFailed time.Time +} + +func (s *strictTransport) reset() { + s.upgradePort = -1 + s.persistenceDuration = -1 + s.preload = false +} + +func (s *strictTransport) expired() bool { + return int(time.Since(s.persistenceReceived).Seconds()) > s.persistenceDuration +} + +func (s *strictTransport) enabled() bool { + return s.upgradePort > 0 +} + +// ErrSTSUpgradeFailed is an error that occurs when a connection that was attempted +// to be upgraded via a strict transport policy, failed. This does not necessarily +// indicate that STS was to blame, but the underlying connection failed for some +// reason. +type ErrSTSUpgradeFailed struct { + Err error +} + +func (e ErrSTSUpgradeFailed) Error() string { + return fmt.Sprintf("fail to upgrade to secure (sts) connection: %v", e.Err) +} + +// notify sends state change notifications so users can update their refs +// when state changes. +func (s *state) notify(c *Client, ntype string) { + c.RunHandlers(&Event{Command: ntype}) +} |
