mirror of
https://github.com/cwinfo/matterbridge.git
synced 2024-11-23 00:01:37 +00:00
Vendor github.com/lrstanley/girc
This commit is contained in:
parent
27e94c438d
commit
e313154134
21
vendor/github.com/lrstanley/girc/LICENSE
generated
vendored
Normal file
21
vendor/github.com/lrstanley/girc/LICENSE
generated
vendored
Normal file
@ -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.
|
518
vendor/github.com/lrstanley/girc/builtin.go
generated
vendored
Normal file
518
vendor/github.com/lrstanley/girc/builtin.go
generated
vendored
Normal file
@ -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, RPL_WELCOME, HandlerFunc(func(c *Client, e Event) {
|
||||||
|
go handleConnect(c, e)
|
||||||
|
}))
|
||||||
|
c.Handlers.register(true, PING, HandlerFunc(handlePING))
|
||||||
|
c.Handlers.register(true, PONG, HandlerFunc(handlePONG))
|
||||||
|
|
||||||
|
if !c.Config.disableTracking {
|
||||||
|
// Joins/parts/anything that may add/remove/rename users.
|
||||||
|
c.Handlers.register(true, JOIN, HandlerFunc(handleJOIN))
|
||||||
|
c.Handlers.register(true, PART, HandlerFunc(handlePART))
|
||||||
|
c.Handlers.register(true, KICK, HandlerFunc(handleKICK))
|
||||||
|
c.Handlers.register(true, QUIT, HandlerFunc(handleQUIT))
|
||||||
|
c.Handlers.register(true, NICK, HandlerFunc(handleNICK))
|
||||||
|
c.Handlers.register(true, RPL_NAMREPLY, HandlerFunc(handleNAMES))
|
||||||
|
|
||||||
|
// Modes.
|
||||||
|
c.Handlers.register(true, MODE, HandlerFunc(handleMODE))
|
||||||
|
c.Handlers.register(true, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
|
||||||
|
|
||||||
|
// WHO/WHOX responses.
|
||||||
|
c.Handlers.register(true, RPL_WHOREPLY, HandlerFunc(handleWHO))
|
||||||
|
c.Handlers.register(true, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
|
||||||
|
|
||||||
|
// Other misc. useful stuff.
|
||||||
|
c.Handlers.register(true, TOPIC, HandlerFunc(handleTOPIC))
|
||||||
|
c.Handlers.register(true, RPL_TOPIC, HandlerFunc(handleTOPIC))
|
||||||
|
c.Handlers.register(true, RPL_MYINFO, HandlerFunc(handleMYINFO))
|
||||||
|
c.Handlers.register(true, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
|
||||||
|
c.Handlers.register(true, RPL_MOTDSTART, HandlerFunc(handleMOTD))
|
||||||
|
c.Handlers.register(true, RPL_MOTD, HandlerFunc(handleMOTD))
|
||||||
|
|
||||||
|
// Keep users lastactive times up to date.
|
||||||
|
c.Handlers.register(true, PRIVMSG, HandlerFunc(updateLastActive))
|
||||||
|
c.Handlers.register(true, NOTICE, HandlerFunc(updateLastActive))
|
||||||
|
c.Handlers.register(true, TOPIC, HandlerFunc(updateLastActive))
|
||||||
|
c.Handlers.register(true, KICK, HandlerFunc(updateLastActive))
|
||||||
|
|
||||||
|
// CAP IRCv3-specific tracking and functionality.
|
||||||
|
c.Handlers.register(true, CAP, HandlerFunc(handleCAP))
|
||||||
|
c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
|
||||||
|
c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY))
|
||||||
|
c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
|
||||||
|
c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags))
|
||||||
|
|
||||||
|
// SASL IRCv3 support.
|
||||||
|
c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL))
|
||||||
|
c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
|
||||||
|
c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nickname collisions.
|
||||||
|
c.Handlers.register(true, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
|
||||||
|
c.Handlers.register(true, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
|
||||||
|
c.Handlers.register(true, 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.RunHandlers(&Event{Command: CONNECTED, Trailing: c.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
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cmd.Nick(c.Config.HandleNickCollide(c.GetNick()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePING helps respond to ping requests from the server.
|
||||||
|
func handlePING(c *Client, e Event) {
|
||||||
|
c.Cmd.Pong(e.Trailing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePONG(c *Client, e Event) {
|
||||||
|
c.conn.lastPong = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleJOIN ensures that the state has updated users and channels.
|
||||||
|
func handleJOIN(c *Client, e Event) {
|
||||||
|
if e.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var channelName string
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
channelName = e.Params[0]
|
||||||
|
} else {
|
||||||
|
channelName = e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Name); !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.Trailing) > 0 {
|
||||||
|
user.Extras.Name = e.Trailing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
if e.Source.Name == c.GetNick() {
|
||||||
|
// 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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel string
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
channel = e.Params[0]
|
||||||
|
} else {
|
||||||
|
channel = e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.state.notify(c, UPDATE_STATE)
|
||||||
|
|
||||||
|
if e.Source.Name == c.GetNick() {
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteChannel(channel)
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteUser(channel, e.Source.Name)
|
||||||
|
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[len(e.Params)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
channel := c.state.lookupChannel(name)
|
||||||
|
if channel == nil {
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.Topic = e.Trailing
|
||||||
|
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) != 7 {
|
||||||
|
// 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.Trailing
|
||||||
|
} else {
|
||||||
|
// Assume RPL_WHOREPLY.
|
||||||
|
ident, host, nick = e.Params[2], e.Params[3], e.Params[5]
|
||||||
|
if len(e.Trailing) > 2 {
|
||||||
|
realname = e.Trailing[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Name, e.Params[0])
|
||||||
|
} else if len(e.Trailing) > 0 {
|
||||||
|
c.state.renameUser(e.Source.Name, e.Trailing)
|
||||||
|
}
|
||||||
|
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.Name == c.GetNick() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteUser("", e.Source.Name)
|
||||||
|
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. 005 is also used for server bounce
|
||||||
|
// related things, so this handler may be triggered during other
|
||||||
|
// situations.
|
||||||
|
|
||||||
|
// Also known as RPL_PROTOCTL.
|
||||||
|
if !strings.HasSuffix(e.Trailing, "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.
|
||||||
|
for i := 1; i < len(e.Params); i++ {
|
||||||
|
j := strings.IndexByte(e.Params[i], 0x3D) // =
|
||||||
|
|
||||||
|
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 len(c.state.motd) != 0 {
|
||||||
|
e.Trailing = "\n" + e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.motd += e.Trailing
|
||||||
|
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[len(e.Params)-1])
|
||||||
|
if channel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(e.Trailing, " ")
|
||||||
|
|
||||||
|
var host, ident, modes, nick string
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
host = s.Host
|
||||||
|
nick = s.Name
|
||||||
|
ident = s.Ident
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.createUser(nick)
|
||||||
|
user := c.state.lookupUser(nick)
|
||||||
|
if user == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
user.addChannel(channel.Name)
|
||||||
|
channel.addUser(nick)
|
||||||
|
|
||||||
|
// Add necessary userhost-in-names data into the user.
|
||||||
|
if host != "" {
|
||||||
|
user.Host = host
|
||||||
|
}
|
||||||
|
if ident != "" {
|
||||||
|
user.Ident = ident
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
639
vendor/github.com/lrstanley/girc/cap.go
generated
vendored
Normal file
639
vendor/github.com/lrstanley/girc/cap.go
generated
vendored
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
// 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"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"multi-prefix": nil,
|
||||||
|
"userhost-in-names": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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][]string {
|
||||||
|
out := make(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]] = strings.Split(parts[i][val+1:], ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCAP attempts to find out what IRCv3 capabilities the server supports.
|
||||||
|
// This will lock further registration until we have acknowledged the
|
||||||
|
// capabilities.
|
||||||
|
func handleCAP(c *Client, e Event) {
|
||||||
|
if len(e.Params) >= 2 && (e.Params[1] == CAP_NEW || e.Params[1] == CAP_DEL) {
|
||||||
|
c.listCAP()
|
||||||
|
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)
|
||||||
|
|
||||||
|
if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS {
|
||||||
|
c.state.Lock()
|
||||||
|
|
||||||
|
caps := parseCap(e.Trailing)
|
||||||
|
|
||||||
|
for k := range caps {
|
||||||
|
if _, ok := possible[k]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(possible[k]) == 0 || len(caps[k]) == 0 {
|
||||||
|
c.state.tmpCap = append(c.state.tmpCap, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var contains bool
|
||||||
|
for i := 0; i < len(caps[k]); i++ {
|
||||||
|
for j := 0; j < len(possible[k]); j++ {
|
||||||
|
if caps[k][i] == possible[k][j] {
|
||||||
|
// Assume we have a matching split value.
|
||||||
|
contains = true
|
||||||
|
goto checkcontains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkcontains:
|
||||||
|
if !contains {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.tmpCap = append(c.state.tmpCap, k)
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
// Indicates if this is a multi-line LS. (2 args means it's the
|
||||||
|
// last LS).
|
||||||
|
if len(e.Params) == 2 {
|
||||||
|
// 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.
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " ")})
|
||||||
|
|
||||||
|
// 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.Lock()
|
||||||
|
c.state.tmpCap = []string{}
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK {
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.enabledCap = strings.Split(e.Trailing, " ")
|
||||||
|
|
||||||
|
// Do we need to do sasl auth?
|
||||||
|
wantsSASL := false
|
||||||
|
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||||
|
if c.state.enabledCap[i] == "sasl" {
|
||||||
|
wantsSASL = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
if wantsSASL {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, Trailing: fmt.Sprintf(
|
||||||
|
"closing connection: invalid %s SASL configuration provided: %s",
|
||||||
|
c.Config.SASL.Method(), e.Trailing,
|
||||||
|
)}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Trailing: "closing connection: " + e.Trailing}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Trailing
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Name)
|
||||||
|
if user != nil {
|
||||||
|
user.Extras.Account = account
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
prefixTag byte = 0x40 // @
|
||||||
|
prefixTagValue byte = 0x3D // =
|
||||||
|
prefixUserTag byte = 0x2B // +
|
||||||
|
tagSeparator byte = 0x3B // ;
|
||||||
|
maxTagLength int = 511 // 510 + @ and " " (space), 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
|
||||||
|
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]] = 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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] < 0x41 || name[i] > 0x5A) && (name[i] < 0x61 || name[i] > 0x7A) && (name[i] < 0x2D || name[i] > 0x39) && name[i] != 0x5F {
|
||||||
|
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] < 0x21 || value[i] > 0x7E || value[i] == 0x3B {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
615
vendor/github.com/lrstanley/girc/client.go
generated
vendored
Normal file
615
vendor/github.com/lrstanley/girc/client.go
generated
vendored
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
// 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"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"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
|
||||||
|
// 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
|
||||||
|
// 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.Lag() 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 prop 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.
|
||||||
|
HandleNickCollide func(oldNick string) (newNick string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 < 21 || conf.Port > 65535 {
|
||||||
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (21-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")
|
||||||
|
|
||||||
|
// ErrDisconnected is called when Config.Retries is less than 1, and we
|
||||||
|
// non-intentionally disconnected from the server.
|
||||||
|
var ErrDisconnected = errors.New("unexpectedly disconnected")
|
||||||
|
|
||||||
|
// ErrInvalidTarget should be returned if the target which you are
|
||||||
|
// attempting to send an event to is invalid or doesn't match RFC spec.
|
||||||
|
type ErrInvalidTarget struct {
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrInvalidTarget) Error() string { return "invalid target: " + e.Target }
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.Debug == nil {
|
||||||
|
c.debug = log.New(ioutil.Discard, "", 0)
|
||||||
|
} else {
|
||||||
|
c.debug = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile)
|
||||||
|
c.debug.Print("initializing debugging")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the caller.
|
||||||
|
c.Handlers = newCaller(c.debug)
|
||||||
|
|
||||||
|
// Give ourselves a new state.
|
||||||
|
c.state = &state{}
|
||||||
|
c.state.reset()
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the network connection to the server, and sends a STOPPED
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
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-choosen 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 net.Conn.
|
||||||
|
func (c *Client) Server() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.Config.Server, 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() (connected 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
return c.state.host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns the active list of channels that the client is in.
|
||||||
|
// Panics if tracking is disabled.
|
||||||
|
func (c *Client) Channels() []string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
channels := make([]string, len(c.state.channels))
|
||||||
|
var i int
|
||||||
|
for channel := range c.state.channels {
|
||||||
|
channels[i] = c.state.channels[channel].Name
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
sort.Strings(channels)
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users returns the active list of users that the client is tracking across
|
||||||
|
// all files. Panics if tracking is disabled.
|
||||||
|
func (c *Client) Users() []string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
users := make([]string, len(c.state.users))
|
||||||
|
var i int
|
||||||
|
for user := range c.state.users {
|
||||||
|
users[i] = c.state.users[user].Nick
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
sort.Strings(users)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
channel := c.state.lookupChannel(name)
|
||||||
|
if channel == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
if nick == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
user := c.state.lookupUser(nick)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInChannel returns true if the client is in channel. Panics if tracking
|
||||||
|
// is disabled.
|
||||||
|
func (c *Client) IsInChannel(channel string) bool {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
_, inChannel := c.state.channels[ToRFC1459(channel)]
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return inChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lag 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) Lag() 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
197
vendor/github.com/lrstanley/girc/cmdhandler/cmd.go
generated
vendored
Normal file
197
vendor/github.com/lrstanley/girc/cmdhandler/cmd.go
generated
vendored
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
package cmdhandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/lrstanley/girc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Input is a wrapper for events, based around private messages.
|
||||||
|
type Input struct {
|
||||||
|
Origin *girc.Event
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command is an IRC command, supporting aliases, help documentation and easy
|
||||||
|
// wrapping for message inputs.
|
||||||
|
type Command struct {
|
||||||
|
// Name of command, e.g. "search" or "ping".
|
||||||
|
Name string
|
||||||
|
// Aliases for the above command, e.g. "s" for search, or "p" for "ping".
|
||||||
|
Aliases []string
|
||||||
|
// Help documentation. Should be in the format "<arg> <arg> [arg] --
|
||||||
|
// something useful here"
|
||||||
|
Help string
|
||||||
|
// MinArgs is the minimum required arguments for the command. Defaults to
|
||||||
|
// 0, which means multiple, or no arguments can be supplied. If set
|
||||||
|
// above 0, this means that the command handler will throw an error asking
|
||||||
|
// the person to check "<prefix>help <command>" for more info.
|
||||||
|
MinArgs int
|
||||||
|
// Fn is the function which is executed when the command is ran from a
|
||||||
|
// private message, or channel.
|
||||||
|
Fn func(*girc.Client, *Input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) genHelp(prefix string) string {
|
||||||
|
out := "{b}" + prefix + c.Name + "{b}"
|
||||||
|
|
||||||
|
if c.Aliases != nil && len(c.Aliases) > 0 {
|
||||||
|
out += " ({b}" + prefix + strings.Join(c.Aliases, "{b}, {b}"+prefix) + "{b})"
|
||||||
|
}
|
||||||
|
|
||||||
|
out += " :: " + c.Help
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdHandler is an irc command parser and execution format which you could
|
||||||
|
// use as an example for building your own version/bot.
|
||||||
|
//
|
||||||
|
// An example of how you would register this with girc:
|
||||||
|
//
|
||||||
|
// ch, err := cmdhandler.New("!")
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ch.Add(&cmdhandler.Command{
|
||||||
|
// Name: "ping",
|
||||||
|
// Help: "Sends a pong reply back to the original user.",
|
||||||
|
// Fn: func(c *girc.Client, input *cmdhandler.Input) {
|
||||||
|
// c.Commands.ReplyTo(*input.Origin, "pong!")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// client.Handlers.AddHandler(girc.PRIVMSG, ch)
|
||||||
|
type CmdHandler struct {
|
||||||
|
prefix string
|
||||||
|
re *regexp.Regexp
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
cmds map[string]*Command
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$`
|
||||||
|
|
||||||
|
// New returns a new CmdHandler based on the specified command prefix. A good
|
||||||
|
// prefix is a single character, and easy to remember/use. E.g. "!", or ".".
|
||||||
|
func New(prefix string) (*CmdHandler, error) {
|
||||||
|
re, err := regexp.Compile(fmt.Sprintf(cmdMatch, regexp.QuoteMeta(prefix)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`)
|
||||||
|
|
||||||
|
// Add registers a new command to the handler. Note that you cannot remove
|
||||||
|
// commands once added, unless you add another CmdHandler to the client.
|
||||||
|
func (ch *CmdHandler) Add(cmd *Command) error {
|
||||||
|
if cmd == nil {
|
||||||
|
return errors.New("nil command provided to CmdHandler")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Name = strings.ToLower(cmd.Name)
|
||||||
|
if !validName.MatchString(cmd.Name) {
|
||||||
|
return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Name, validName.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Aliases != nil {
|
||||||
|
for i := 0; i < len(cmd.Aliases); i++ {
|
||||||
|
cmd.Aliases[i] = strings.ToLower(cmd.Aliases[i])
|
||||||
|
if !validName.MatchString(cmd.Aliases[i]) {
|
||||||
|
return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Aliases[i], validName.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.MinArgs < 0 {
|
||||||
|
cmd.MinArgs = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.mu.Lock()
|
||||||
|
defer ch.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := ch.cmds[cmd.Name]; ok {
|
||||||
|
return fmt.Errorf("command already registered: %s", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.cmds[cmd.Name] = cmd
|
||||||
|
|
||||||
|
// Since we'd be storing pointers, duplicates do not matter.
|
||||||
|
for i := 0; i < len(cmd.Aliases); i++ {
|
||||||
|
if _, ok := ch.cmds[cmd.Aliases[i]]; ok {
|
||||||
|
return fmt.Errorf("alias already registered: %s", cmd.Aliases[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.cmds[cmd.Aliases[i]] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute satisfies the girc.Handler interface.
|
||||||
|
func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) {
|
||||||
|
if event.Source == nil || event.Command != girc.PRIVMSG {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := ch.re.FindStringSubmatch(event.Trailing)
|
||||||
|
if len(parsed) != 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invCmd := strings.ToLower(parsed[1])
|
||||||
|
args := strings.Split(parsed[2], " ")
|
||||||
|
if len(args) == 1 && args[0] == "" {
|
||||||
|
args = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.mu.Lock()
|
||||||
|
defer ch.mu.Unlock()
|
||||||
|
|
||||||
|
if invCmd == "help" {
|
||||||
|
if len(args) == 0 {
|
||||||
|
client.Cmd.ReplyTo(event, girc.Fmt("type '{b}!help {blue}<command>{c}{b}' to optionally get more info about a specific command."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args[0] = strings.ToLower(args[0])
|
||||||
|
|
||||||
|
if _, ok := ch.cmds[args[0]]; !ok {
|
||||||
|
client.Cmd.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch.cmds[args[0]].Help == "" {
|
||||||
|
client.Cmd.ReplyTof(event, girc.Fmt("there is no help documentation for {b}%q{b}"), args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Cmd.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, ok := ch.cmds[invCmd]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) < cmd.MinArgs {
|
||||||
|
client.Cmd.ReplyTof(event, girc.Fmt("not enough arguments supplied for {b}%q{b}. try '{b}%shelp %s{b}'?"), invCmd, ch.prefix, invCmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in := &Input{
|
||||||
|
Origin: &event,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
go cmd.Fn(client, in)
|
||||||
|
}
|
398
vendor/github.com/lrstanley/girc/commands.go
generated
vendored
Normal file
398
vendor/github.com/lrstanley/girc/commands.go
generated
vendored
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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) error {
|
||||||
|
if !IsValidNick(name) {
|
||||||
|
return &ErrInvalidTarget{Target: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) error {
|
||||||
|
// 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 !IsValidChannel(channels[i]) {
|
||||||
|
return &ErrInvalidTarget{Target: channels[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer+","+channels[i]) > max {
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
||||||
|
buffer = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer) == 0 {
|
||||||
|
buffer = channels[i]
|
||||||
|
} else {
|
||||||
|
buffer += "," + channels[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(channels)-1 {
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinKey attempts to enter an IRC channel with a password.
|
||||||
|
func (cmd *Commands) JoinKey(channel, password string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part leaves an IRC channel.
|
||||||
|
func (cmd *Commands) Part(channel, message string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PartMessage leaves an IRC channel with a specified leave message.
|
||||||
|
func (cmd *Commands) PartMessage(channel, message string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCTCP sends a CTCP request to target. Note that this method uses
|
||||||
|
// PRIVMSG specifically.
|
||||||
|
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error {
|
||||||
|
out := encodeCTCPRaw(ctcpType, message)
|
||||||
|
if out == "" {
|
||||||
|
return errors.New("invalid CTCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Message(target, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCTCPf sends a CTCP request to target using a specific format. Note that
|
||||||
|
// this method uses PRIVMSG specifically.
|
||||||
|
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error {
|
||||||
|
return 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.
|
||||||
|
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error {
|
||||||
|
return 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) error {
|
||||||
|
out := encodeCTCPRaw(ctcpType, message)
|
||||||
|
if out == "" {
|
||||||
|
return errors.New("invalid CTCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Notice(target, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message sends a PRIVMSG to target (either channel, service, or user).
|
||||||
|
func (cmd *Commands) Message(target, message string) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messagef sends a formated PRIVMSG to target (either channel, service, or
|
||||||
|
// user).
|
||||||
|
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
|
||||||
|
return 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().
|
||||||
|
func (cmd *Commands) Reply(event Event, message string) error {
|
||||||
|
if event.Source == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||||
|
return 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().
|
||||||
|
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
|
||||||
|
return 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().
|
||||||
|
func (cmd *Commands) ReplyTo(event Event, message string) error {
|
||||||
|
if event.Source == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||||
|
return 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().
|
||||||
|
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
|
||||||
|
return 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) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Params: []string{target},
|
||||||
|
Trailing: fmt.Sprintf("\001ACTION %s\001", message),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
|
||||||
|
// service, or user).
|
||||||
|
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error {
|
||||||
|
return 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) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noticef sends a formated NOTICE to target (either channel, service, or
|
||||||
|
// user).
|
||||||
|
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error {
|
||||||
|
return cmd.Notice(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRaw sends a raw string back to the server, without carriage returns
|
||||||
|
// or newlines.
|
||||||
|
func (cmd *Commands) SendRaw(raw string) error {
|
||||||
|
e := ParseEvent(raw)
|
||||||
|
if e == nil {
|
||||||
|
return errors.New("invalid event: " + raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(e)
|
||||||
|
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}, Trailing: 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(target string) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whois sends a WHOIS query to the server, targeted at a specific user.
|
||||||
|
// as WHOIS is a bit slower, you may want to use WHO for brief user info.
|
||||||
|
func (cmd *Commands) Whois(nick string) error {
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, nick, reason string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reason != "" {
|
||||||
|
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite sends a INVITE query to the server, to invite nick to channel.
|
||||||
|
func (cmd *Commands) Invite(channel, nick string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) error {
|
||||||
|
if len(channels) == 0 {
|
||||||
|
cmd.c.Send(&Event{Command: LIST})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 !IsValidChannel(channels[i]) {
|
||||||
|
return &ErrInvalidTarget{Target: channels[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer+","+channels[i]) > max {
|
||||||
|
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
||||||
|
buffer = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer) == 0 {
|
||||||
|
buffer = channels[i]
|
||||||
|
} else {
|
||||||
|
buffer += "," + channels[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(channels)-1 {
|
||||||
|
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
||||||
|
// you want back.
|
||||||
|
func (cmd *Commands) Whowas(nick string, amount int) error {
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
|
||||||
|
return nil
|
||||||
|
}
|
566
vendor/github.com/lrstanley/girc/conn.go
generated
vendored
Normal file
566
vendor/github.com/lrstanley/girc/conn.go
generated
vendored
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
// 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
|
||||||
|
pingDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (*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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.SSL {
|
||||||
|
var tlsConn net.Conn
|
||||||
|
tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
if mock == nil {
|
||||||
|
// Validate info, and actually make the connection.
|
||||||
|
c.debug.Printf("connecting to %s...", c.Server())
|
||||||
|
conn, err := newConn(c.Config, dialer, c.Server())
|
||||||
|
if err != nil {
|
||||||
|
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.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, "*", "*"}, Trailing: c.Config.Name})
|
||||||
|
|
||||||
|
// Send a virtual event allowing hooks for successful socket connection.
|
||||||
|
c.RunHandlers(&Event{Command: INITIALIZED, Trailing: c.Server()})
|
||||||
|
|
||||||
|
// Wait for the first error.
|
||||||
|
var result error
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
c.debug.Print("received request to close, beginning clean up")
|
||||||
|
c.RunHandlers(&Event{Command: STOPPED, Trailing: c.Server()})
|
||||||
|
case err := <-errs:
|
||||||
|
c.debug.Print("received error, beginning clean up")
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if !c.Config.AllowFlood {
|
||||||
|
<-time.After(c.conn.rate(event.Len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.GlobalFormat && event.Trailing != "" &&
|
||||||
|
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
|
||||||
|
event.Trailing = Fmt(event.Trailing)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
|
||||||
|
c.writeDelay = 0
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
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 c.state.enabledCap[i] == "message-tags" {
|
||||||
|
in = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
if !in {
|
||||||
|
event.Tags = Tags{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the event.
|
||||||
|
if event.Sensitive {
|
||||||
|
c.debug.Printf("> %s ***redacted***", event.Command)
|
||||||
|
} else {
|
||||||
|
c.debug.Print("> ", StripRaw(event.String()))
|
||||||
|
}
|
||||||
|
if c.Config.Out != nil {
|
||||||
|
if pretty, ok := event.Pretty(); ok {
|
||||||
|
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 during ping to server" }
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
errs <- ErrTimedOut{
|
||||||
|
TimeSinceSuccess: time.Since(c.conn.lastPong),
|
||||||
|
LastPong: c.conn.lastPong,
|
||||||
|
LastPing: c.conn.lastPing,
|
||||||
|
Delay: c.Config.PingDelay,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
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()))
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
338
vendor/github.com/lrstanley/girc/contants.go
generated
vendored
Normal file
338
vendor/github.com/lrstanley/girc/contants.go
generated
vendored
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
// 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_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)
|
||||||
|
STOPPED = "CLIENT_STOPPED" // occurs when Client.Stop() has been called
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
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"
|
||||||
|
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_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"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
)
|
288
vendor/github.com/lrstanley/girc/ctcp.go
generated
vendored
Normal file
288
vendor/github.com/lrstanley/girc/ctcp.go
generated
vendored
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
// 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 match a valid CTCP.
|
||||||
|
func decodeCTCP(e *Event) *CTCPEvent {
|
||||||
|
// http://www.irchelp.org/protocol/ctcpspec.html
|
||||||
|
|
||||||
|
// Must be targeting a user/channel, AND trailing must have
|
||||||
|
// DELIM+TAG+DELIM minimum (at least 3 chars).
|
||||||
|
if len(e.Params) != 1 || len(e.Trailing) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Command != PRIVMSG && e.Command != NOTICE) || !IsValidNick(e.Params[0]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Trailing[0] != ctcpDelim || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip delimiters.
|
||||||
|
text := e.Trailing[1 : len(e.Trailing)-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] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) {
|
||||||
|
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] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) {
|
||||||
|
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 len(cmd) <= 0 {
|
||||||
|
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 {
|
||||||
|
// Send a ERRMSG reply, if we know who sent it.
|
||||||
|
if event.Source != nil && IsValidNick(event.Source.Name) {
|
||||||
|
client.Cmd.SendCTCPReply(event.Source.Name, 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] < 0x41 || cmd[i] > 0x5A) && (cmd[i] < 0x30 || cmd[i] > 0x39) {
|
||||||
|
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.Name, CTCP_PING, ctcp.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPPong replies with a pong.
|
||||||
|
func handleCTCPPong(client *Client, ctcp CTCPEvent) {
|
||||||
|
if ctcp.Reply {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, 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.Name, CTCP_VERSION, client.Config.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Cmd.SendCTCPReplyf(
|
||||||
|
ctcp.Source.Name, 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.Name, 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.Name, 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.Name, CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
||||||
|
}
|
12
vendor/github.com/lrstanley/girc/doc.go
generated
vendored
Normal file
12
vendor/github.com/lrstanley/girc/doc.go
generated
vendored
Normal file
@ -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
|
550
vendor/github.com/lrstanley/girc/event.go
generated
vendored
Normal file
550
vendor/github.com/lrstanley/girc/event.go
generated
vendored
Normal file
@ -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 (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventSpace byte = 0x20 // Separator.
|
||||||
|
maxLength = 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'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *Source `json:"source"` // The source of the event.
|
||||||
|
Tags Tags `json:"tags"` // IRCv3 style message tags. Only use if network supported.
|
||||||
|
Command string `json:"command"` // the IRC command, e.g. JOIN, PRIVMSG, KILL.
|
||||||
|
Params []string `json:"params"` // parameters to the command. Commonly nickname, channel, etc.
|
||||||
|
Trailing string `json:"trailing"` // any trailing data. e.g. with a PRIVMSG, this is the message text.
|
||||||
|
EmptyTrailing bool `json:"empty_trailing"` // if true, trailing prefix (:) will be added even if Event.Trailing is empty.
|
||||||
|
Sensitive bool `json:"sensitive"` // if the message is sensitive (e.g. and should not be logged).
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
|
||||||
|
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])
|
||||||
|
raw = raw[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if 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.Split(raw[j:], string(eventSpace))
|
||||||
|
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.Split(raw[j:i-1], string(eventSpace))
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Trailing = raw[i+1:]
|
||||||
|
|
||||||
|
// We need to re-encode the trailing argument even if it was empty.
|
||||||
|
if len(e.Trailing) <= 0 {
|
||||||
|
e.EmptyTrailing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
Command: e.Command,
|
||||||
|
Trailing: e.Trailing,
|
||||||
|
EmptyTrailing: e.EmptyTrailing,
|
||||||
|
Sensitive: e.Sensitive,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
length += len(e.Params)
|
||||||
|
|
||||||
|
for i := 0; i < len(e.Params); i++ {
|
||||||
|
length += len(e.Params[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Trailing) > 0 || e.EmptyTrailing {
|
||||||
|
// Include prefix and space.
|
||||||
|
length += len(e.Trailing) + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
buffer.WriteByte(eventSpace)
|
||||||
|
buffer.WriteString(strings.Join(e.Params, string(eventSpace)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Trailing) > 0 || e.EmptyTrailing {
|
||||||
|
buffer.WriteByte(eventSpace)
|
||||||
|
buffer.WriteByte(messagePrefix)
|
||||||
|
buffer.WriteString(e.Trailing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the limit the buffer length.
|
||||||
|
if buffer.Len() > (maxLength) {
|
||||||
|
buffer.Truncate(maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := buffer.Bytes()
|
||||||
|
|
||||||
|
// Strip newlines and carriage returns.
|
||||||
|
for i := 0; i < len(out); i++ {
|
||||||
|
if out[i] == 0x0A || out[i] == 0x0D {
|
||||||
|
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 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == ERROR {
|
||||||
|
return fmt.Sprintf("[*] an error occurred: %s", e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Source == nil {
|
||||||
|
if e.Command != PRIVMSG && e.Command != NOTICE {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Params) > 0 && len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[>] writing %s [%s]: %s", strings.ToLower(e.Command), strings.Join(e.Params, ", "), e.Trailing), true
|
||||||
|
} else if len(e.Params) > 0 {
|
||||||
|
return fmt.Sprintf("[>] writing %s [%s]", strings.ToLower(e.Command), strings.Join(e.Params, ", ")), true
|
||||||
|
} else if len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[>] writing %s: %s", strings.ToLower(e.Command), e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == INITIALIZED {
|
||||||
|
return fmt.Sprintf("[*] connection to %s initialized", e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == CONNECTED {
|
||||||
|
return fmt.Sprintf("[*] successfully connected to %s", e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 {
|
||||||
|
if ctcp := decodeCTCP(e); ctcp != nil {
|
||||||
|
if ctcp.Reply {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] (%s) %s", strings.Join(e.Params, ","), e.Source.Name, e.Trailing), 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.Trailing, 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.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == QUIT {
|
||||||
|
return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Trailing), 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.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == NICK && len(e.Params) == 1 {
|
||||||
|
return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Params[0]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == TOPIC && len(e.Params) > 0 {
|
||||||
|
return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[len(e.Params)-1], e.Source.Name, e.Trailing), 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.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Trailing), 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 == RPL_TOPIC && len(e.Params) > 0 && len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[*] topic for %s is: %s", e.Params[len(e.Params)-1], e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me).
|
||||||
|
func (e *Event) IsAction() bool {
|
||||||
|
if e.Source == nil || e.Command != PRIVMSG || len(e.Trailing) < 9 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 || 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 || 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.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Trailing[8 : len(e.Trailing)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
messagePrefix byte = 0x3A // ":" -- prefix or last argument
|
||||||
|
prefixIdent byte = 0x21 // "!" -- username
|
||||||
|
prefixHost byte = 0x40 // "@" -- 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.
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 len(s.Ident) <= 0 && len(s.Host) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
350
vendor/github.com/lrstanley/girc/format.go
generated
vendored
Normal file
350
vendor/github.com/lrstanley/girc/format.go
generated
vendored
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
// 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 = 0x7B // {
|
||||||
|
fmtCloseChar = 0x7D // }
|
||||||
|
)
|
||||||
|
|
||||||
|
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] != 0x2c && (text[i] <= 0x41 || text[i] >= 0x5a) && (text[i] <= 0x61 || text[i] >= 0x7a) {
|
||||||
|
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.Replace(text, "{"+color+"}", "", -1)
|
||||||
|
}
|
||||||
|
for code := range fmtCodes {
|
||||||
|
text = strings.Replace(text, "{"+code+"}", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is really the only fastest way of doing this (marginably better than
|
||||||
|
// actually trying to parse it manually.)
|
||||||
|
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)
|
||||||
|
|
||||||
|
// 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.Replace(text, code, "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidChannel validates if channel is an RFC complaint 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 "*" in the prefix list, as this is commonly used (e.g. ZNC)
|
||||||
|
if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, 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] == 0x21 {
|
||||||
|
if len(channel) < 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for valid ID
|
||||||
|
for i := 1; i < 6; i++ {
|
||||||
|
if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) {
|
||||||
|
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 nickame. 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 len(nick) <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nick = ToRFC1459(nick)
|
||||||
|
|
||||||
|
// Check the first index. Some characters aren't allowed for the first
|
||||||
|
// index of an IRC nickname.
|
||||||
|
if nick[0] < 0x41 || nick[0] > 0x7D {
|
||||||
|
// a-z, A-Z, and _\[]{}^|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(nick); i++ {
|
||||||
|
if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D {
|
||||||
|
// 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 len(name) <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
name = ToRFC1459(name)
|
||||||
|
|
||||||
|
// "~" is prepended (commonly) if there was no ident server response.
|
||||||
|
if name[0] == 0x7E {
|
||||||
|
// 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] < 0x41 || name[0] > 0x4A) && (name[0] < 0x61 || name[0] > 0x7A) && (name[0] < 0x30 || name[0] > 0x39) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(name); i++ {
|
||||||
|
if (name[i] < 0x41 || name[i] > 0x7D) && (name[i] < 0x30 || name[i] > 0x39) && name[i] != 0x2D && name[i] != 0x2E {
|
||||||
|
// 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.
|
||||||
|
func ToRFC1459(input string) (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])
|
||||||
|
}
|
484
vendor/github.com/lrstanley/girc/handler.go
generated
vendored
Normal file
484
vendor/github.com/lrstanley/girc/handler.go
generated
vendored
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
// 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.
|
||||||
|
c.debug.Print("< " + StripRaw(event.String()))
|
||||||
|
if c.Config.Out != nil {
|
||||||
|
if pretty, ok := event.Pretty(); ok {
|
||||||
|
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular wildcard handlers.
|
||||||
|
c.Handlers.exec(ALL_EVENTS, c, event.Copy())
|
||||||
|
|
||||||
|
// Then regular handlers.
|
||||||
|
c.Handlers.exec(event.Command, 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, 0x3A)
|
||||||
|
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
|
||||||
|
// handler types themselves or the handlers are executed.
|
||||||
|
func (c *Caller) exec(command string, 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] {
|
||||||
|
stack = append(stack, execStack{c.internal[command][cuid], cuid})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aaand then external handlers.
|
||||||
|
if _, ok := c.external[command]; ok {
|
||||||
|
for cuid := range c.external[command] {
|
||||||
|
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) {
|
||||||
|
c.debug.Printf("executing handler %s for event %s (%d of %d)", stack[index].cuid, command, index+1, len(stack))
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil {
|
||||||
|
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack[index].Execute(client, *event)
|
||||||
|
|
||||||
|
c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack))
|
||||||
|
wg.Done()
|
||||||
|
}(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 len(cmd) == 0 || len(uid) == 0 {
|
||||||
|
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 bool, cmd string, handler Handler) (cuid string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
cuid = c.register(internal, 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 bool, cmd string, handler Handler) (cuid string) {
|
||||||
|
var uid string
|
||||||
|
|
||||||
|
cmd = strings.ToUpper(cmd)
|
||||||
|
|
||||||
|
if internal {
|
||||||
|
if _, ok := c.internal[cmd]; !ok {
|
||||||
|
c.internal[cmd] = map[string]Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cuid, uid = c.cuid(cmd, 20)
|
||||||
|
c.internal[cmd][uid] = handler
|
||||||
|
} else {
|
||||||
|
if _, ok := c.external[cmd]; !ok {
|
||||||
|
c.external[cmd] = map[string]Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cuid, uid = c.cuid(cmd, 20)
|
||||||
|
c.external[cmd][uid] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
_, file, line, _ := runtime.Caller(3)
|
||||||
|
|
||||||
|
c.debug.Printf("registering handler for %q with cuid %q (internal: %t) from: %s:%d", cmd, cuid, internal, 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, 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, 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, cmd, HandlerFunc(func(client *Client, event Event) {
|
||||||
|
// Setting up background-based handlers this way allows us to get
|
||||||
|
// clean call stacks for use with panic recovery.
|
||||||
|
go func() {
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil {
|
||||||
|
defer recoverHandlerPanic(client, &event, "goroutine", 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(client, event)
|
||||||
|
}()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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. Additionally, 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{}) {
|
||||||
|
var uid string
|
||||||
|
cuid, uid = c.cuid(cmd, 20)
|
||||||
|
|
||||||
|
done = make(chan struct{})
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if _, ok := c.external[cmd]; !ok {
|
||||||
|
c.external[cmd] = map[string]Handler{}
|
||||||
|
}
|
||||||
|
c.external[cmd][uid] = HandlerFunc(func(client *Client, event Event) {
|
||||||
|
// Setting up background-based handlers this way allows us to get
|
||||||
|
// clean call stacks for use with panic recovery.
|
||||||
|
go func() {
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil {
|
||||||
|
defer recoverHandlerPanic(client, &event, "tmp-goroutine", 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
remove := handler(client, event)
|
||||||
|
if remove {
|
||||||
|
if ok := c.Remove(cuid); ok {
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if deadline > 0 {
|
||||||
|
go func() {
|
||||||
|
<-time.After(deadline)
|
||||||
|
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 string
|
||||||
|
var line int
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
_, file, line, ok = runtime.Caller(skip)
|
||||||
|
|
||||||
|
err := &HandlerError{
|
||||||
|
Event: *event,
|
||||||
|
ID: id,
|
||||||
|
File: file,
|
||||||
|
Line: line,
|
||||||
|
Panic: perr,
|
||||||
|
Stack: debug.Stack(),
|
||||||
|
callOk: ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Config.RecoverFunc(client, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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())
|
||||||
|
}
|
550
vendor/github.com/lrstanley/girc/modes.go
generated
vendored
Normal file
550
vendor/github.com/lrstanley/girc/modes.go
generated
vendored
Normal file
@ -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 len(c.args) == 0 {
|
||||||
|
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 len(c.modes[i].args) == 0 {
|
||||||
|
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 new []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 {
|
||||||
|
new = append(new, modes[i])
|
||||||
|
isin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isin {
|
||||||
|
new = append(new, 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(new); j++ {
|
||||||
|
if modes[i].name == new[j].name {
|
||||||
|
isin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isin {
|
||||||
|
new = append(new, modes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.modes = new
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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] == 0x2B {
|
||||||
|
add = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if flags[i] == 0x2D {
|
||||||
|
add = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := CMode{
|
||||||
|
name: flags[i],
|
||||||
|
add: add,
|
||||||
|
}
|
||||||
|
|
||||||
|
hasArgs, isSetting := c.hasArg(add, flags[i])
|
||||||
|
if hasArgs && len(args) >= argCount+1 {
|
||||||
|
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] != 0x2C && (raw[i] < 0x41 || raw[i] > 0x5A) && (raw[i] < 0x61 || raw[i] > 0x7A) {
|
||||||
|
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] != 0x28 { // (.
|
||||||
|
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] == 0x29 { // ).
|
||||||
|
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 || len(modes[i].args) == 0 {
|
||||||
|
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, append bool) {
|
||||||
|
if !append {
|
||||||
|
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
|
||||||
|
}
|
489
vendor/github.com/lrstanley/girc/state.go
generated
vendored
Normal file
489
vendor/github.com/lrstanley/girc/state.go
generated
vendored
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
// 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 (
|
||||||
|
"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 []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 []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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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})
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset resets the state back to it's original form.
|
||||||
|
func (s *state) reset() {
|
||||||
|
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 = []string{}
|
||||||
|
s.motd = ""
|
||||||
|
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 {
|
||||||
|
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.StringsAreSorted(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 {
|
||||||
|
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(nick string) (ok bool) {
|
||||||
|
if _, ok := s.users[ToRFC1459(nick)]; ok {
|
||||||
|
// User already exists.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.users[ToRFC1459(nick)] = &User{
|
||||||
|
Nick: nick,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
vendor/manifest
vendored
8
vendor/manifest
vendored
@ -278,6 +278,14 @@
|
|||||||
"path": "/random",
|
"path": "/random",
|
||||||
"notests": true
|
"notests": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"importpath": "github.com/lrstanley/girc",
|
||||||
|
"repository": "https://github.com/lrstanley/girc",
|
||||||
|
"vcs": "git",
|
||||||
|
"revision": "055075db54ebd311be5946efb3f62502846089ff",
|
||||||
|
"branch": "master",
|
||||||
|
"notests": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"importpath": "github.com/matrix-org/gomatrix",
|
"importpath": "github.com/matrix-org/gomatrix",
|
||||||
"repository": "https://github.com/matrix-org/gomatrix",
|
"repository": "https://github.com/matrix-org/gomatrix",
|
||||||
|
Loading…
Reference in New Issue
Block a user