Compare commits
109 Commits
split-mess
...
master
Author | SHA1 | Date | |
---|---|---|---|
f9a80d019b | |||
623ca6ffc1 | |||
568e9652da | |||
633a5dea16 | |||
0d38e8f3d9 | |||
ee1c72a7f5 | |||
5f71c07dee | |||
d91de1c3c0 | |||
3458ae3d6d | |||
0ae5183ad5 | |||
c1560dfd88 | |||
9083cf55cf | |||
32f2b078c3 | |||
61ef265c6c | |||
0b5ba7050c | |||
0dec6ca8e2 | |||
af4ae39f57 | |||
18243520dc | |||
f6b2020909 | |||
78e9032f25 | |||
c2a726248a | |||
00308d0ce6 | |||
4be5266426 | |||
|
9664730c78 | ||
|
6681863d12 | ||
|
aaebfc1f09 | ||
|
3a46659a82 | ||
|
887ab30b90 | ||
|
8487a7de15 | ||
|
b5af8a2128 | ||
dea3455490 | |||
12e6cfbe3b | |||
1051473e2b | |||
5619a7527f | |||
41ffc42937 | |||
2378deab84 | |||
e4014626b5 | |||
e835e5898d | |||
f6902689f1 | |||
6fc7f6e1d7 | |||
8b3710579e | |||
eee810320c | |||
86271f76fa | |||
ac38ef0258 | |||
640d183c1b | |||
8ce24dc17c | |||
5e68e20bbb | |||
3c9ed46490 | |||
507dd2f271 | |||
290eedf446 | |||
b9d39b20fe | |||
de1dae9799 | |||
9c72236c15 | |||
b8186e2144 | |||
23cea998f1 | |||
fa2aba1ef2 | |||
2d8ab65b6d | |||
02e997f314 | |||
6ca2202d58 | |||
20ec8a60e6 | |||
4276b04ba7 | |||
5e2fd661f3 | |||
36151a10b5 | |||
22cd701da8 | |||
90d8c80bcd | |||
923b7b1bb1 | |||
5ea3252b9b | |||
1eacc3e108 | |||
4beeca47ab | |||
a999837230 | |||
e0299a3766 | |||
3db5c2c0ab | |||
3629c0de73 | |||
7f85567f20 | |||
87a2ab50c1 | |||
58c1d27f2c | |||
baf2e0dedf | |||
79e2583dda | |||
ce2eeb8072 | |||
ff65f6fa03 | |||
b9856ab64e | |||
|
147f0ff775 | ||
7f101f3656 | |||
410e8686ab | |||
ea8bd7c774 | |||
f17812708d | |||
e809c349d0 | |||
53bcfb4ee5 | |||
d32dd5e8a0 | |||
c46e976ff3 | |||
9724515d2b | |||
b84d822ec5 | |||
c4ff5a6022 | |||
6ef31ffdcb | |||
606b5a452c | |||
e50801a78e | |||
9065b0d6ab | |||
1556e364c0 | |||
59d0b7398e | |||
4c1d73dcbd | |||
01ce96b07a | |||
|
771323f162 | ||
|
4219526e1d | ||
|
f97c533ce1 | ||
|
b7e90b27e4 | ||
|
28ef073485 | ||
|
8929b1a531 | ||
|
2cb73c3772 | ||
|
288d953b28 |
25
.github/workflows/go.yml
vendored
Normal file
25
.github/workflows/go.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -race -v ./...
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.idea
|
||||
*.save
|
||||
*.swp
|
||||
corpus/
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
- tip
|
||||
before_install:
|
||||
- go get -v golang.org/x/lint/golint
|
||||
script:
|
||||
- $HOME/gopath/bin/golint -min_confidence 0.9 -set_exit_status
|
||||
- GORACE="exitcode=1 halt_on_error=1" go test -v -coverprofile=coverage.txt -race -timeout 3m -count 3 -cpu 1,4
|
||||
- go vet -v .
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- irc.byteirc.org#/dev/null
|
||||
template:
|
||||
- "%{repository} #%{build_number} %{branch}/%{commit}: %{author} -- %{message}
|
||||
%{build_url}"
|
||||
on_success: change
|
||||
on_failure: change
|
||||
skip_join: false
|
@ -2,31 +2,24 @@
|
||||
|
||||
## Issue submission
|
||||
|
||||
* When submitting an issue or bug report, please ensure to provide as much
|
||||
information as possible, please ensure that you are running on the latest
|
||||
stable version (tagged), or when using master, provide the specific commit
|
||||
being used.
|
||||
* Provide the minimum needed viable source to replicate the problem.
|
||||
* When submitting an issue or bug report, please ensure to provide as much information as possible, please ensure that
|
||||
you are running on the latest stable version (tagged), or when using master, provide the specific commit being used.
|
||||
* Provide the minimum needed viable source to replicate the problem.
|
||||
|
||||
## Pull requests
|
||||
|
||||
To review what is currently being worked on, or looked into, feel free to head
|
||||
over to the [issues list](../../issues).
|
||||
To review what is currently being worked on, or looked into, feel free to head over to the [issues list](../../issues).
|
||||
|
||||
Below are a few guidelines if you would like to contribute. Keep the code
|
||||
clean, standardized, and much of the quality should match Golang's standard
|
||||
library and common idioms.
|
||||
Below are a few guidelines if you would like to contribute. Keep the code clean, standardized, and much of the quality
|
||||
should match Golang's standard library and common idioms.
|
||||
|
||||
* Always test using the latest Go version.
|
||||
* Always use `gofmt` before committing anything.
|
||||
* Always have proper documentation before committing.
|
||||
* Keep the same whitespacing, documentation, and newline format as the
|
||||
rest of the project.
|
||||
* Only use 3rd party libraries if necessary. If only a small portion of
|
||||
the library is needed, simply rewrite it within the library to prevent
|
||||
useless imports.
|
||||
* Also see [golang/go/wiki/CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
* Always test using the latest Go version.
|
||||
* Always use `gofmt` before committing anything.
|
||||
* Always have proper documentation before committing.
|
||||
* Keep the same whitespacing, documentation, and newline format as the rest of the project.
|
||||
* Only use 3rd party libraries if necessary. If only a small portion of the library is needed, simply rewrite it within
|
||||
the library to prevent useless imports.
|
||||
* Also see [golang/go/wiki/CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
|
||||
If you would like to assist, and the pull request is quite large and/or it has
|
||||
the potential of being a breaking change, please open an issue first so it can
|
||||
be discussed.
|
||||
If you would like to assist, and the pull request is quite large and/or it has the potential of being a breaking change,
|
||||
please open an issue first so it can be discussed.
|
||||
|
1
LICENSE
1
LICENSE
@ -1,6 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Liam Stanley <me@liamstanley.io>
|
||||
Copyright (c) 2016 yung innanet <kayos@tcp.directs>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
82
README.md
82
README.md
@ -1,67 +1,59 @@
|
||||
<p align="center"><a href="https://godoc.org/github.com/lrstanley/girc"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
|
||||
<p align="center">girc, a flexible IRC library for Go</p>
|
||||
<p align="center"><a href="https://tcp.ac/i/G5OTn" target="_blank"><img width="270" src="https://tcp.ac/i/G5OTn"></a></p>
|
||||
<p align="center">girc-atomic, a terrifying fork of an IRC library for Go</p>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/lrstanley/girc"><img src="https://travis-ci.org/lrstanley/girc.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://codecov.io/gh/lrstanley/girc"><img src="https://codecov.io/gh/lrstanley/girc/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||
<a href="https://godoc.org/github.com/lrstanley/girc"><img src="https://godoc.org/github.com/lrstanley/girc?status.png" alt="GoDoc"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/lrstanley/girc"><img src="https://goreportcard.com/badge/github.com/lrstanley/girc" alt="Go Report Card"></a>
|
||||
<a href="https://byteirc.org/channel/%23%2Fdev%2Fnull"><img src="https://img.shields.io/badge/ByteIRC-%23%2Fdev%2Fnull-blue.svg" alt="IRC Chat"></a>
|
||||
<a href="https://godoc.org/github.com/yunginnanet/girc-atomic"><img src="https://godoc.org/github.com/yunginnanet/girc-atomic?status.png" alt="GoDoc"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/yunginnanet/girc-atomic"><img src="https://goreportcard.com/badge/github.com/yunginnanet/girc-atomic" alt="Go Report Card"></a>
|
||||
<a href="ircs://ircd.chat:6697/#tcpdirect"><img src="https://img.shields.io/badge/ircd.chat-%23tcpdirect-blue.svg" alt="IRC Chat"></a>
|
||||
<a href="https://github.com/yunginnanet/girc-atomic/actions/workflows/go.yml"><img src="https://github.com/yunginnanet/girc-atomic/actions/workflows/go.yml/badge.svg?branch=master" alt="Build Status"></a>
|
||||
</p>
|
||||
|
||||
## Fork changes
|
||||
|
||||
[Click here to see the changes in girc-atomic vs girc](https://github.com/lrstanley/girc/compare/master...yunginnanet:master)
|
||||
|
||||
## Status
|
||||
|
||||
**girc is fairly close to marking the 1.0.0 endpoint, which will be tagged as
|
||||
necessary, so you will be able to use this with care knowing the specific tag
|
||||
you're using won't have breaking changes**
|
||||
### ₜₕₑ ₛₖy ᵢₛ 𝆑ₐₗₗᵢₙg ʇɥǝ sʞʎ ᴉs ⅎɐʅʅᴉuƃ
|
||||
### 𝚝𝚑𝚎𝚢 𝚜𝚑𝚘𝚞𝚕𝚍 𝚑𝚊𝚟𝚎 𝚕𝚒𝚜𝚝𝚎𝚗𝚎𝚍
|
||||
### ʇɥǝ sʞʎ ᴉs ⅎɐʅʅᴉuƃ ₜₕₑ ₛₖy ᵢₛ 𝆑ₐₗₗᵢₙg
|
||||
|
||||
## Features
|
||||
|
||||
- Focuses on simplicity, yet tries to still be flexible.
|
||||
- Only requires [standard library packages](https://godoc.org/github.com/lrstanley/girc?imports)
|
||||
- Event based triggering/responses ([example](https://godoc.org/github.com/lrstanley/girc#ex-package--Commands), and [CTCP too](https://godoc.org/github.com/lrstanley/girc#Commands.SendCTCP)!)
|
||||
- [Documentation](https://godoc.org/github.com/lrstanley/girc) is _mostly_ complete.
|
||||
- Focuses on ~~simplicity~~ ʀᴀɪɴɪɴɢ ʜᴇʟʟғɪʀᴇ, yet tries to still be flexible.
|
||||
- Only requires ~~[standard library packages](https://godoc.org/github.com/yunginnanet/girc-atomic?imports)~~ a total destruction of a 100 mile radius.
|
||||
- Event based triggering/responses ([example](https://godoc.org/github.com/yunginnanet/girc-atomic#ex-package--Commands), and [CTCP too](https://godoc.org/github.com/yunginnanet/girc-atomic#Commands.SendCTCP)!)
|
||||
- [Documentation](https://godoc.org/github.com/yunginnanet/girc-atomic) is _mostly_ complete.
|
||||
- Support for almost all of the [IRCv3 spec](http://ircv3.net/software/libraries.html).
|
||||
- SASL Auth (currently only `PLAIN` and `EXTERNAL` is support by default,
|
||||
- SASL Auth (currently only `PLAIN` and `EXTERNAL` is support by default,
|
||||
however you can simply implement `SASLMech` yourself to support additional
|
||||
mechanisms.)
|
||||
- Message tags (things like `account-tag` on by default)
|
||||
- `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://github.com/lrstanley/girc/blob/master/cap.go) for more info).
|
||||
- Message tags (things like `account-tag` on by default)
|
||||
- `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://github.com/yunginnanet/girc-atomic/blob/master/cap.go) for more info).
|
||||
- Channel and user tracking. Easily find what users are in a channel, if a
|
||||
user is away, or if they are authenticated (if the server supports it!)
|
||||
- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/github.com/lrstanley/girc#Client.LookupChannel), [LookupUser](https://godoc.org/github.com/lrstanley/girc#Client.LookupUser), [GetServerOption (ISUPPORT)](https://godoc.org/github.com/lrstanley/girc#Client.GetServerOption), etc.)
|
||||
- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/github.com/yunginnanet/girc-atomic#Client.LookupChannel), [LookupUser](https://godoc.org/github.com/yunginnanet/girc-atomic#Client.LookupUser), [GetServerOpt (ISUPPORT)](https://godoc.org/github.com/yunginnanet/girc-atomic#Client.GetServerOpt), etc.)
|
||||
- Built-in support for things you would commonly have to implement yourself.
|
||||
- Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/github.com/lrstanley/girc#Config).)
|
||||
- Event/message rate limiting.
|
||||
- Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/github.com/lrstanley/girc#IsValidChannel), [IsValidNick](https://godoc.org/github.com/lrstanley/girc#IsValidNick), etc.)
|
||||
- CTCP handling and auto-responses ([CTCP](https://godoc.org/github.com/lrstanley/girc#CTCP))
|
||||
- And more!
|
||||
|
||||
## Installing
|
||||
|
||||
$ go get -u github.com/lrstanley/girc
|
||||
|
||||
## Examples
|
||||
|
||||
See [the examples](https://godoc.org/github.com/lrstanley/girc#example-package--Bare)
|
||||
within the documentation for real-world usecases. Here are a few real-world
|
||||
usecases/examples/projects which utilize girc:
|
||||
|
||||
| Project | Description |
|
||||
| --- | --- |
|
||||
| [nagios-check-ircd](https://github.com/lrstanley/nagios-check-ircd) | Nagios utility for monitoring the health of an ircd |
|
||||
| [nagios-notify-irc](https://github.com/lrstanley/nagios-notify-irc) | Nagios utility for sending alerts to one or many channels/networks |
|
||||
| [matterbridge](https://github.com/42wim/matterbridge) | bridge between mattermost, IRC, slack, discord (and many others) with REST API |
|
||||
|
||||
Working on a project and want to add it to the list? Submit a pull request!
|
||||
- Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/github.com/yunginnanet/girc-atomic#Config).)
|
||||
- Event/message rate limiting.
|
||||
- Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/github.com/yunginnanet/girc-atomic#IsValidChannel), [IsValidNick](https://godoc.org/github.com/yunginnanet/girc-atomic#IsValidNick), etc.)
|
||||
- CTCP handling and auto-responses ([CTCP](https://godoc.org/github.com/yunginnanet/girc-atomic#CTCP))
|
||||
- Utilizes atomics and concurrent maps to reduce backpressure in multi-client usage. (fork)
|
||||
- Additional CTCP handlers and customization. (fork)
|
||||
- ??????
|
||||
- PROFIT!!!1!
|
||||
|
||||
## Contributing
|
||||
|
||||
Please review the [CONTRIBUTING](CONTRIBUTING.md) doc for submitting issues/a guide
|
||||
on submitting pull requests and helping out.
|
||||
~~Please review the [CONTRIBUTING](CONTRIBUTING.md) doc for submitting issues/a guide
|
||||
on submitting pull requests and helping out.~~
|
||||
|
||||
**OH GOD PLEASE MAKE IT STOP**
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2016 Liam Stanley <me@liamstanley.io>
|
||||
Copyright (c) 2022 yung innanet <kayos@tcp.direct>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -81,7 +73,9 @@ on submitting pull requests and helping out.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
girc artwork licensed under [CC 3.0](http://creativecommons.org/licenses/by/3.0/) based on Renee French under Creative Commons 3.0 Attributions.
|
||||
girc artwork licensed under [CC 3.0](http://creativecommons.org/licenses/by/3.0/) based on Renee French under Creative Commons 3.0 Attributions
|
||||
|
||||
...and then later defiled by [some idiot](https://github.com/yunginnanet).
|
||||
|
||||
## References
|
||||
|
||||
|
381
builtin.go
381
builtin.go
@ -5,75 +5,88 @@
|
||||
package girc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/araddon/dateparse"
|
||||
)
|
||||
|
||||
// registerBuiltin sets up built-in handlers, based on client
|
||||
// configuration.
|
||||
func (c *Client) registerBuiltins() {
|
||||
c.debug.Print("registering built-in handlers")
|
||||
c.Handlers.mu.Lock()
|
||||
|
||||
// Built-in things that should always be supported.
|
||||
c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
|
||||
c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
|
||||
c.Handlers.register(true, false, PONG, HandlerFunc(handlePONG))
|
||||
|
||||
if !c.Config.disableTracking {
|
||||
// Joins/parts/anything that may add/remove/rename users.
|
||||
c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
|
||||
c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
|
||||
c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
|
||||
c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
|
||||
c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
|
||||
c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
|
||||
|
||||
// Modes.
|
||||
c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
|
||||
c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
|
||||
|
||||
// WHO/WHOX responses.
|
||||
c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
|
||||
c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
|
||||
|
||||
// Other misc. useful stuff.
|
||||
c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
|
||||
c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
|
||||
c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
|
||||
c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
|
||||
c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
|
||||
c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
|
||||
|
||||
// Keep users lastactive times up to date.
|
||||
c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
|
||||
|
||||
// CAP IRCv3-specific tracking and functionality.
|
||||
c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
|
||||
c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
|
||||
c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
|
||||
c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
|
||||
c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
|
||||
|
||||
// SASL IRCv3 support.
|
||||
c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
|
||||
c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
|
||||
c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
|
||||
}
|
||||
|
||||
// Nickname collisions.
|
||||
c.Handlers.register(true, false, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
|
||||
c.Handlers.register(true, false, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
|
||||
c.Handlers.register(true, false, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
|
||||
|
||||
c.Handlers.mu.Unlock()
|
||||
if c.Config.disableTracking {
|
||||
return
|
||||
}
|
||||
|
||||
// Joins/parts/anything that may add/remove/rename users.
|
||||
c.Handlers.register(true, false, JOIN, HandlerFunc(handleJOIN))
|
||||
c.Handlers.register(true, false, PART, HandlerFunc(handlePART))
|
||||
c.Handlers.register(true, false, KICK, HandlerFunc(handleKICK))
|
||||
c.Handlers.register(true, false, QUIT, HandlerFunc(handleQUIT))
|
||||
c.Handlers.register(true, false, NICK, HandlerFunc(handleNICK))
|
||||
c.Handlers.register(true, false, RPL_NAMREPLY, HandlerFunc(handleNAMES))
|
||||
|
||||
// Modes.
|
||||
c.Handlers.register(true, false, MODE, HandlerFunc(handleMODE))
|
||||
c.Handlers.register(true, false, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
|
||||
|
||||
// Channel creation time.
|
||||
c.Handlers.register(true, false, RPL_CREATIONTIME, HandlerFunc(handleCREATIONTIME))
|
||||
|
||||
// WHO/WHOX responses.
|
||||
c.Handlers.register(true, false, RPL_WHOREPLY, HandlerFunc(handleWHO))
|
||||
c.Handlers.register(true, false, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
|
||||
|
||||
// Other misc. useful stuff.
|
||||
c.Handlers.register(true, false, TOPIC, HandlerFunc(handleTOPIC))
|
||||
c.Handlers.register(true, false, RPL_TOPIC, HandlerFunc(handleTOPIC))
|
||||
c.Handlers.register(true, false, RPL_YOURHOST, HandlerFunc(handleYOURHOST))
|
||||
c.Handlers.register(true, false, RPL_CREATED, HandlerFunc(handleCREATED))
|
||||
c.Handlers.register(true, false, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
|
||||
c.Handlers.register(true, false, RPL_LUSERCHANNELS, HandlerFunc(handleLUSERCHANNELS)) // 254
|
||||
c.Handlers.register(true, false, RPL_GLOBALUSERS, HandlerFunc(handleGLOBALUSERS)) // 266
|
||||
c.Handlers.register(true, false, RPL_LOCALUSERS, HandlerFunc(handleLOCALUSERS)) // 265
|
||||
c.Handlers.register(true, false, RPL_LUSEROP, HandlerFunc(handleLUSEROP)) // 252
|
||||
c.Handlers.register(true, false, RPL_MOTDSTART, HandlerFunc(handleMOTD))
|
||||
c.Handlers.register(true, false, RPL_MOTD, HandlerFunc(handleMOTD))
|
||||
// c.Handlers.register(true, false, RPL_MYINFO, HandlerFunc(handleMYINFO))
|
||||
|
||||
// Keep users lastactive times up to date.
|
||||
c.Handlers.register(true, false, PRIVMSG, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, NOTICE, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, TOPIC, HandlerFunc(updateLastActive))
|
||||
c.Handlers.register(true, false, KICK, HandlerFunc(updateLastActive))
|
||||
|
||||
// CAP IRCv3-specific tracking and functionality.
|
||||
c.Handlers.register(true, false, CAP, HandlerFunc(handleCAP))
|
||||
c.Handlers.register(true, false, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
|
||||
c.Handlers.register(true, false, CAP_AWAY, HandlerFunc(handleAWAY))
|
||||
c.Handlers.register(true, false, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
|
||||
c.Handlers.register(true, false, ALL_EVENTS, HandlerFunc(handleTags))
|
||||
|
||||
// SASL IRCv3 support.
|
||||
c.Handlers.register(true, false, AUTHENTICATE, HandlerFunc(handleSASL))
|
||||
c.Handlers.register(true, false, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
|
||||
c.Handlers.register(true, false, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLFAIL, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, ERR_SASLABORTED, HandlerFunc(handleSASLError))
|
||||
c.Handlers.register(true, false, RPL_SASLMECHS, HandlerFunc(handleSASLError))
|
||||
return
|
||||
}
|
||||
|
||||
// handleConnect is a helper function which lets the client know that enough
|
||||
@ -85,30 +98,46 @@ func handleConnect(c *Client, e Event) {
|
||||
// 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.nick.Store(e.Params[0])
|
||||
c.state.notify(c, UPDATE_GENERAL)
|
||||
split := strings.Split(e.Params[1], " ")
|
||||
search:
|
||||
for i, artifact := range split {
|
||||
switch strings.ToLower(artifact) {
|
||||
case "welcome", "to":
|
||||
continue
|
||||
case "the":
|
||||
if len(split) < i {
|
||||
break search
|
||||
}
|
||||
c.IRCd.Network.Store(split[i+1])
|
||||
break search
|
||||
default:
|
||||
break search
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
c.mu.RLock()
|
||||
server := c.server()
|
||||
c.mu.RUnlock()
|
||||
c.RunHandlers(&Event{Command: CONNECTED, Params: []string{server}})
|
||||
}
|
||||
|
||||
// nickCollisionHandler helps prevent the client from having conflicting
|
||||
// nicknames with another bot, user, etc.
|
||||
//
|
||||
//goland:noinspection GoUnusedParameter
|
||||
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()))
|
||||
newNick := c.Config.HandleNickCollide(c.GetNick())
|
||||
if newNick != "" {
|
||||
c.Cmd.Nick(newNick)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePING helps respond to ping requests from the server.
|
||||
@ -116,10 +145,9 @@ func handlePING(c *Client, e Event) {
|
||||
c.Cmd.Pong(e.Last())
|
||||
}
|
||||
|
||||
//goland:noinspection GoUnusedParameter
|
||||
func handlePONG(c *Client, e Event) {
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPong = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
c.conn.lastPong.Store(time.Now())
|
||||
}
|
||||
|
||||
// handleJOIN ensures that the state has updated users and channels.
|
||||
@ -130,12 +158,9 @@ func handleJOIN(c *Client, e Event) {
|
||||
|
||||
channelName := e.Params[0]
|
||||
|
||||
c.state.Lock()
|
||||
|
||||
channel := c.state.lookupChannel(channelName)
|
||||
if channel == nil {
|
||||
if ok := c.state.createChannel(channelName); !ok {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@ -144,8 +169,7 @@ func handleJOIN(c *Client, e Event) {
|
||||
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user == nil {
|
||||
if ok := c.state.createUser(e.Source); !ok {
|
||||
c.state.Unlock()
|
||||
if _, ok := c.state.createUser(e.Source); !ok {
|
||||
return
|
||||
}
|
||||
user = c.state.lookupUser(e.Source.Name)
|
||||
@ -153,8 +177,8 @@ func handleJOIN(c *Client, e Event) {
|
||||
|
||||
defer c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
channel.addUser(user.Nick)
|
||||
user.addChannel(channel.Name)
|
||||
channel.addUser(user.Nick.Load().(string), user)
|
||||
user.addChannel(channel.Name, channel)
|
||||
|
||||
// Assume extended-join (ircv3).
|
||||
if len(e.Params) >= 2 {
|
||||
@ -166,7 +190,6 @@ func handleJOIN(c *Client, e Event) {
|
||||
user.Extras.Name = e.Params[2]
|
||||
}
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
if e.Source.ID() == c.GetID() {
|
||||
// If it's us, don't just add our user to the list. Run a WHO which
|
||||
@ -178,10 +201,8 @@ func handleJOIN(c *Client, e Event) {
|
||||
|
||||
// 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()
|
||||
c.state.ident.Store(e.Source.Ident)
|
||||
c.state.host.Store(e.Source.Host)
|
||||
return
|
||||
}
|
||||
|
||||
@ -195,7 +216,11 @@ func handlePART(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.debug.Println("handlePart")
|
||||
defer c.debug.Println("handlePart done for " + e.Params[0])
|
||||
|
||||
// TODO: does this work if it's not the bot?
|
||||
// er yes, but needs a test case
|
||||
|
||||
channel := e.Params[0]
|
||||
|
||||
@ -205,16 +230,42 @@ func handlePART(c *Client, e Event) {
|
||||
|
||||
defer c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
if chn := c.LookupChannel(channel); chn != nil {
|
||||
chn.UserList.Remove(e.Source.ID())
|
||||
c.debug.Println(fmt.Sprintf("removed: %s, new count: %d", e.Source.ID(), chn.Len()))
|
||||
} else {
|
||||
c.debug.Println("failed to lookup channel: " + channel)
|
||||
}
|
||||
|
||||
if e.Source.ID() == c.GetID() {
|
||||
c.state.Lock()
|
||||
c.state.deleteChannel(channel)
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
c.state.deleteUser(channel, e.Source.ID())
|
||||
c.state.Unlock()
|
||||
}
|
||||
|
||||
// handleCREATIONTIME handles incoming TOPIC events and keeps channel tracking info
|
||||
// updated with the latest channel topic.
|
||||
func handleCREATIONTIME(c *Client, e Event) {
|
||||
var created string
|
||||
var name string
|
||||
switch len(e.Params) {
|
||||
case 0, 1, 2:
|
||||
return
|
||||
default:
|
||||
name = e.Params[1]
|
||||
created = e.Params[2]
|
||||
break
|
||||
}
|
||||
|
||||
channel := c.state.lookupChannel(name)
|
||||
if channel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel.Created = created
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleTOPIC handles incoming TOPIC events and keeps channel tracking info
|
||||
@ -230,15 +281,14 @@ func handleTOPIC(c *Client, e Event) {
|
||||
name = e.Params[1]
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
channel := c.state.lookupChannel(name)
|
||||
if channel == nil {
|
||||
c.state.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel.Topic = e.Last()
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
@ -283,22 +333,35 @@ func handleWHO(c *Client, e Event) {
|
||||
}
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(nick)
|
||||
if user == nil {
|
||||
c.state.Unlock()
|
||||
usr, _ := c.state.createUser(&Source{nick, ident, host})
|
||||
usr.Extras.Name = realname
|
||||
if account != "0" {
|
||||
usr.Extras.Account = account
|
||||
}
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
return
|
||||
}
|
||||
|
||||
user.Host = host
|
||||
user.Ident = ident
|
||||
user.Ident.Store(ident)
|
||||
|
||||
str := strs.Get()
|
||||
str.MustWriteString(user.Nick.Load().(string))
|
||||
str.MustWriteString("!")
|
||||
str.MustWriteString(user.Ident.Load().(string))
|
||||
str.MustWriteString("@")
|
||||
str.MustWriteString(user.Host)
|
||||
user.Mask.Store(str.String())
|
||||
strs.MustPut(str)
|
||||
|
||||
user.Extras.Name = realname
|
||||
|
||||
if account != "0" {
|
||||
user.Extras.Account = account
|
||||
}
|
||||
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
@ -313,16 +376,16 @@ func handleKICK(c *Client, e Event) {
|
||||
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
|
||||
@ -332,12 +395,10 @@ func handleNICK(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
// renameUser updates the LastActive time automatically.
|
||||
if len(e.Params) >= 1 {
|
||||
c.state.renameUser(e.Source.ID(), e.Last())
|
||||
}
|
||||
c.state.Unlock()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
@ -351,32 +412,101 @@ func handleQUIT(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
c.state.deleteUser("", e.Source.ID())
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleMYINFO handles incoming MYINFO events -- these are commonly used
|
||||
// to tell us what the server name is, what version of software is being used
|
||||
// as well as what channel and user modes are being used on the server.
|
||||
func handleMYINFO(c *Client, e Event) {
|
||||
// Malformed or odd output. As this can differ strongly between networks,
|
||||
// just skip it.
|
||||
if len(e.Params) < 3 {
|
||||
func handleGLOBALUSERS(c *Client, e Event) {
|
||||
cusers, err := strconv.Atoi(e.Params[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
musers, err := strconv.Atoi(e.Params[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.IRCd.UserCount = cusers
|
||||
c.IRCd.MaxUserCount = musers
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
c.state.serverOptions["SERVER"] = e.Params[1]
|
||||
c.state.serverOptions["VERSION"] = e.Params[2]
|
||||
c.state.Unlock()
|
||||
func handleLOCALUSERS(c *Client, e Event) {
|
||||
cusers, err := strconv.Atoi(e.Params[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
musers, err := strconv.Atoi(e.Params[2])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.IRCd.LocalUserCount = cusers
|
||||
c.IRCd.LocalMaxUserCount = musers
|
||||
}
|
||||
|
||||
func handleLUSERCHANNELS(c *Client, e Event) {
|
||||
ccount, err := strconv.Atoi(e.Params[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.IRCd.ChannelCount = ccount
|
||||
}
|
||||
|
||||
func handleLUSEROP(c *Client, e Event) {
|
||||
ocount, err := strconv.Atoi(e.Params[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.IRCd.OperCount = ocount
|
||||
}
|
||||
|
||||
// handleCREATED handles incoming CREATED events.
|
||||
// This is commonly used to tell us when the IRC daemon was compiled.
|
||||
func handleCREATED(c *Client, e Event) {
|
||||
split := strings.Split(e.Params[1], " ")
|
||||
days := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
||||
found := -1
|
||||
for i, word := range split {
|
||||
for _, day := range days {
|
||||
if word == day+"," {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if found == -1 {
|
||||
return
|
||||
}
|
||||
compiled, err := dateparse.ParseAny(strings.Join(split[found:], " "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.IRCd.Compiled = compiled
|
||||
c.state.notify(c, UPDATE_GENERAL)
|
||||
}
|
||||
|
||||
// handleYOURHOST handles incoming YOURHOST events.
|
||||
// This is commonly used to tell us details on the currently connected leaf.
|
||||
func handleYOURHOST(c *Client, e Event) {
|
||||
var host = ""
|
||||
var ver = ""
|
||||
const prefix = "Your host is "
|
||||
const suffix = " running version "
|
||||
if strings.Contains(e.Params[1], prefix) && strings.Contains(e.Params[1], ",") {
|
||||
s := strings.TrimPrefix(e.Params[1], prefix)
|
||||
split := strings.Split(s, ",")
|
||||
host = split[0]
|
||||
ver = strings.Replace(split[1], suffix, "", 1)
|
||||
}
|
||||
if len(host)+len(ver) == 0 {
|
||||
return
|
||||
}
|
||||
c.IRCd.Host = host
|
||||
c.IRCd.Version = ver
|
||||
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.
|
||||
// events. This commonly contains the date of the daemon's compilation.
|
||||
func handleISUPPORT(c *Client, e Event) {
|
||||
// Must be a ISUPPORT-based message.
|
||||
|
||||
@ -390,45 +520,46 @@ func handleISUPPORT(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
// Skip the first parameter, as it's our nickname, and the last, as it's the doc.
|
||||
for i := 1; i < len(e.Params)-1; i++ {
|
||||
j := strings.IndexByte(e.Params[i], '=')
|
||||
for i := range e.Params {
|
||||
split := strings.Split(e.Params[i], "=")
|
||||
|
||||
if j < 1 || (j+1) == len(e.Params[i]) {
|
||||
c.state.serverOptions[e.Params[i]] = ""
|
||||
if len(split) != 2 {
|
||||
c.state.serverOptions.Set(e.Params[i], "")
|
||||
continue
|
||||
}
|
||||
|
||||
name := e.Params[i][0:j]
|
||||
val := e.Params[i][j+1:]
|
||||
c.state.serverOptions[name] = val
|
||||
if len(split[0]) < 1 || len(split[1]) < 1 {
|
||||
c.state.serverOptions.Set(e.Params[i], "")
|
||||
continue
|
||||
}
|
||||
|
||||
if split[0] == "NETWORK" {
|
||||
c.state.network.Store(split[1])
|
||||
}
|
||||
|
||||
c.state.serverOptions.Set(split[0], split[1])
|
||||
}
|
||||
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 {
|
||||
if c.state.motd != "" {
|
||||
c.state.motd += "\n"
|
||||
}
|
||||
c.state.motd += e.Last()
|
||||
c.state.Unlock()
|
||||
}
|
||||
|
||||
// handleNAMES handles incoming NAMES queries, of which lists all users in
|
||||
@ -448,9 +579,8 @@ func handleNAMES(c *Client, e Event) {
|
||||
|
||||
var modes, nick string
|
||||
var ok bool
|
||||
s := &Source{}
|
||||
var s *Source
|
||||
|
||||
c.state.Lock()
|
||||
for i := 0; i < len(parts); i++ {
|
||||
modes, nick, ok = parseUserPrefix(parts[i])
|
||||
if !ok {
|
||||
@ -480,15 +610,14 @@ func handleNAMES(c *Client, e Event) {
|
||||
continue
|
||||
}
|
||||
|
||||
user.addChannel(channel.Name)
|
||||
channel.addUser(s.ID())
|
||||
user.addChannel(channel.Name, channel)
|
||||
channel.addUser(s.ID(), user)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -501,15 +630,11 @@ func updateLastActive(c *Client, e Event) {
|
||||
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()
|
||||
}
|
||||
|
39
cap.go
39
cap.go
@ -49,9 +49,7 @@ var possibleCap = map[string][]string{
|
||||
const capServerTimeFormat = "2006-01-02T15:04:05.999Z"
|
||||
|
||||
func (c *Client) listCAP() {
|
||||
if !c.Config.disableTracking {
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}})
|
||||
}
|
||||
c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}})
|
||||
}
|
||||
|
||||
func possibleCapList(c *Client) map[string][]string {
|
||||
@ -64,7 +62,7 @@ func possibleCapList(c *Client) map[string][]string {
|
||||
if !c.Config.DisableSTS && !c.Config.SSL {
|
||||
// If fallback supported, and we failed recently, don't try negotiating STS.
|
||||
// ONLY do this fallback if we're expired (primarily useful during the first
|
||||
// sts negotation).
|
||||
// sts negotiation).
|
||||
if time.Since(c.state.sts.lastFailed) < 5*time.Minute && !c.Config.DisableSTSFallback {
|
||||
c.debug.Println("skipping strict transport policy negotiation; failed within the last 5 minutes")
|
||||
} else {
|
||||
@ -106,7 +104,7 @@ func parseCap(raw string) map[string]map[string]string {
|
||||
if j < 0 {
|
||||
out[parts[i][:val]][option] = ""
|
||||
} else {
|
||||
out[parts[i][:val]][option[:j]] = option[j+1 : len(option)]
|
||||
out[parts[i][:val]][option[:j]] = option[j+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,9 +121,8 @@ func handleCAP(c *Client, e Event) {
|
||||
|
||||
if len(e.Params) >= 2 && e.Params[1] == CAP_DEL {
|
||||
caps := parseCap(e.Last())
|
||||
for cap := range caps {
|
||||
// TODO: test the deletion.
|
||||
delete(c.state.enabledCap, cap)
|
||||
for capab := range caps {
|
||||
c.state.enabledCap.Remove(capab)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -194,12 +191,12 @@ func handleCAP(c *Client, e Event) {
|
||||
|
||||
if len(e.Params) == 3 && e.Params[1] == CAP_ACK {
|
||||
enabled := strings.Split(e.Last(), " ")
|
||||
for _, cap := range enabled {
|
||||
if val, ok := c.state.tmpCap[cap]; ok {
|
||||
c.state.enabledCap[cap] = val
|
||||
} else {
|
||||
c.state.enabledCap[cap] = nil
|
||||
for _, capab := range enabled {
|
||||
if val, ok := c.state.tmpCap[capab]; ok {
|
||||
c.state.enabledCap.Set(capab, val)
|
||||
continue
|
||||
}
|
||||
c.state.enabledCap.Remove(capab)
|
||||
}
|
||||
|
||||
// Anything client side that needs to be setup post-capability-acknowledgement,
|
||||
@ -207,9 +204,8 @@ func handleCAP(c *Client, e Event) {
|
||||
|
||||
// Handle STS, and only if it's something specifically we enabled (client
|
||||
// may choose to disable girc automatic STS, and do it themselves).
|
||||
if sts, sok := c.state.enabledCap["sts"]; sok && !c.Config.DisableSTS {
|
||||
if sts, sok := c.state.enabledCap.Get("sts"); sok && !c.Config.DisableSTS {
|
||||
var isError bool
|
||||
|
||||
// Some things are updated in the policy depending on if the current
|
||||
// connection is over tls or not.
|
||||
var hasTLSConnection bool
|
||||
@ -288,7 +284,7 @@ func handleCAP(c *Client, e Event) {
|
||||
// due to cap-notify, we can re-evaluate what we can support.
|
||||
c.state.tmpCap = make(map[string]map[string]string)
|
||||
|
||||
if _, ok := c.state.enabledCap["sasl"]; ok && c.Config.SASL != nil {
|
||||
if _, ok := c.state.enabledCap.Get("sasl"); ok && c.Config.SASL != nil {
|
||||
c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
|
||||
// Don't "CAP END", since we want to authenticate.
|
||||
return
|
||||
@ -309,25 +305,24 @@ func handleCHGHOST(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user != nil {
|
||||
user.Ident = e.Params[0]
|
||||
user.Ident.Store(e.Params[0])
|
||||
user.Host = e.Params[1]
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both
|
||||
// when users are no longer away, or when they are away.
|
||||
func handleAWAY(c *Client, e Event) {
|
||||
c.state.Lock()
|
||||
|
||||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user != nil {
|
||||
user.Extras.Away = e.Last()
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
@ -345,11 +340,9 @@ func handleACCOUNT(c *Client, e Event) {
|
||||
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)
|
||||
}
|
||||
|
@ -120,7 +120,6 @@ func handleSASL(c *Client, e Event) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleSASLError(c *Client, e Event) {
|
||||
|
14
cap_tags.go
14
cap_tags.go
@ -24,12 +24,11 @@ func handleTags(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.Lock()
|
||||
user := c.state.lookupUser(e.Source.ID())
|
||||
if user != nil {
|
||||
user.Extras.Account = account
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
@ -52,9 +51,12 @@ 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
|
||||
//
|
||||
// @aaa=bbb;ccc;example.com/ddd=eee
|
||||
//
|
||||
// NOT:
|
||||
// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
|
||||
//
|
||||
// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
|
||||
//
|
||||
// Technically, there is a length limit of 4096, but the server should reject
|
||||
// tag messages longer than this.
|
||||
@ -250,10 +252,6 @@ func (t Tags) Get(key string) (tag string, success bool) {
|
||||
// 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)
|
||||
}
|
||||
|
47
cap_test.go
47
cap_test.go
@ -16,6 +16,7 @@ func TestCapSupported(t *testing.T) {
|
||||
User: "user",
|
||||
SASL: &SASLPlain{User: "test", Pass: "example"},
|
||||
SupportedCaps: map[string][]string{"example": nil},
|
||||
// Debug: os.Stdout,
|
||||
})
|
||||
|
||||
var ok bool
|
||||
@ -33,26 +34,36 @@ func TestCapSupported(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCap(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want map[string]map[string]string
|
||||
}{
|
||||
{in: "sts=port=6697,duration=1234567890,preload", want: map[string]map[string]string{"sts": {"duration": "1234567890", "preload": "", "port": "6697"}}},
|
||||
{in: "userhost-in-names", want: map[string]map[string]string{"userhost-in-names": nil}},
|
||||
{in: "userhost-in-names test2", want: map[string]map[string]string{"userhost-in-names": nil, "test2": nil}},
|
||||
{in: "example/name=test", want: map[string]map[string]string{"example/name": {"test": ""}}},
|
||||
{
|
||||
in: "userhost-in-names example/name example/name2=test=1,test2=true",
|
||||
want: map[string]map[string]string{
|
||||
"userhost-in-names": nil,
|
||||
"example/name": nil,
|
||||
"example/name2": {"test": "1", "test2": "true"},
|
||||
},
|
||||
var testsParseCap = []struct {
|
||||
in string
|
||||
want map[string]map[string]string
|
||||
}{
|
||||
{in: "sts=port=6697,duration=1234567890,preload", want: map[string]map[string]string{"sts": {"duration": "1234567890", "preload": "", "port": "6697"}}},
|
||||
{in: "userhost-in-names", want: map[string]map[string]string{"userhost-in-names": nil}},
|
||||
{in: "userhost-in-names test2", want: map[string]map[string]string{"userhost-in-names": nil, "test2": nil}},
|
||||
{in: "example/name=test", want: map[string]map[string]string{"example/name": {"test": ""}}},
|
||||
{
|
||||
in: "userhost-in-names example/name example/name2=test=1,test2=true",
|
||||
want: map[string]map[string]string{
|
||||
"userhost-in-names": nil,
|
||||
"example/name": nil,
|
||||
"example/name2": {"test": "1", "test2": "true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func FuzzParseCap(f *testing.F) {
|
||||
for _, tc := range testsParseCap {
|
||||
f.Add(tc.in)
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
_ = parseCap(orig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseCap(t *testing.T) {
|
||||
for _, tt := range testsParseCap {
|
||||
got := parseCap(tt.in)
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
@ -104,7 +115,7 @@ func TestTagGetSetCount(t *testing.T) {
|
||||
}
|
||||
|
||||
// Add a hidden ascii value at the end to make it invalid.
|
||||
if err := e.Tags.Set("key", "invalid-value"+string(0x08)); err == nil {
|
||||
if err := e.Tags.Set("key", "invalid-value\b"); err == nil {
|
||||
t.Fatal("tag set of invalid value should have returned error")
|
||||
}
|
||||
}
|
||||
|
299
client.go
299
client.go
@ -7,10 +7,10 @@ package girc
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@ -19,7 +19,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
)
|
||||
|
||||
// Client contains all of the information necessary to run a single IRC
|
||||
@ -47,6 +50,10 @@ type Client struct {
|
||||
// so multiple threads aren't trying to connect at the same time, and
|
||||
// vice versa.
|
||||
mu sync.RWMutex
|
||||
|
||||
// IRCd encapsulates IRC Server details.
|
||||
IRCd Server
|
||||
|
||||
// stop is used to communicate with Connect(), letting it know that the
|
||||
// client wishes to cancel/close.
|
||||
stop context.CancelFunc
|
||||
@ -57,6 +64,32 @@ type Client struct {
|
||||
conn *ircConn
|
||||
// debug is used if a writer is supplied for Client.Config.Debugger.
|
||||
debug *log.Logger
|
||||
|
||||
atom uint32
|
||||
}
|
||||
|
||||
// Server contains information about the IRC server that the client is connected to.
|
||||
type Server struct {
|
||||
// Network is the name of the IRC network we are connected to as acquired by 001.
|
||||
Network atomic.Value
|
||||
// Version is the software version of the IRC daemon as acquired by 004.
|
||||
Version string
|
||||
// Host is the hostname/id/IP of the leaf, as acquired by 002.
|
||||
Host string
|
||||
// compiled is the reported date the server was compiled on as acquired by 003.
|
||||
Compiled time.Time
|
||||
// UserCount is the amount of online users currently on this network as acquired by 251.
|
||||
UserCount int
|
||||
// MaxUserCount is the amount of online users currently on this network as acquired by 251.
|
||||
MaxUserCount int
|
||||
// LocalUserCount is the amount of online users currently on this leaf as acquired by 265.
|
||||
LocalUserCount int
|
||||
// LocalMaxUserCount is the maximum amount of users that have been on this leaf as acquired by 265.
|
||||
LocalMaxUserCount int
|
||||
// OperCount is the amount of opers currently online as acquired by 252.
|
||||
OperCount int
|
||||
// ChannelCount is the amount of channels formed as acquired by 254.
|
||||
ChannelCount int
|
||||
}
|
||||
|
||||
// Config contains configuration options for an IRC client
|
||||
@ -146,9 +179,20 @@ type Config struct {
|
||||
// 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.
|
||||
// response to a CTCP VERSION. A default message will be sent otherwise.
|
||||
Version string
|
||||
// ClientInfo is the application ClientInfo code information that will be used in
|
||||
// response to a CTCP CLIENTINFO. No response will be sent if this is not set.
|
||||
ClientInfo string
|
||||
// UserInfo is the user information that will be used in
|
||||
// response to a CTCP USERINFO. No response will be sent if this is not set.
|
||||
UserInfo string
|
||||
// Finger is the client information that will be used in
|
||||
// response to a CTCP FINGER. A default message will be sent otherwise.
|
||||
Finger string
|
||||
// Source is the application source code information that will be used in
|
||||
// response to a CTCP SOURCE. A default message will be sent otherwise.
|
||||
Source 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
|
||||
@ -168,6 +212,9 @@ type Config struct {
|
||||
// an invalid nickname. For example, if "test" is already in use, or is
|
||||
// blocked by the network/a service, the client will try and use "test_",
|
||||
// then it will attempt "test__", "test___", and so on.
|
||||
//
|
||||
// If HandleNickCollide returns an empty string, the client will not
|
||||
// attempt to fix nickname collisions, and you must handle this yourself.
|
||||
HandleNickCollide func(oldNick string) (newNick string)
|
||||
}
|
||||
|
||||
@ -177,13 +224,13 @@ type Config struct {
|
||||
// server.
|
||||
//
|
||||
// Client expectations:
|
||||
// - Perform any proxy resolution.
|
||||
// - Check the reverse DNS and forward DNS match.
|
||||
// - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
|
||||
// - Perform any proxy resolution.
|
||||
// - Check the reverse DNS and forward DNS match.
|
||||
// - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
|
||||
//
|
||||
// More information:
|
||||
// - https://ircv3.net/specs/extensions/webirc.html
|
||||
// - https://kiwiirc.com/docs/webirc
|
||||
// - https://ircv3.net/specs/extensions/webirc.html
|
||||
// - https://kiwiirc.com/docs/webirc
|
||||
type WebIRC struct {
|
||||
// Password that authenticates the WEBIRC command from this client.
|
||||
Password string
|
||||
@ -229,10 +276,10 @@ func (conf *Config) isValid() error {
|
||||
}
|
||||
|
||||
if !IsValidNick(conf.Nick) {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")}
|
||||
return &ErrInvalidConfig{Conf: *conf, err: fmt.Errorf("bad nickname specified: %s", conf.Nick)}
|
||||
}
|
||||
if !IsValidUser(conf.User) {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")}
|
||||
return &ErrInvalidConfig{Conf: *conf, err: fmt.Errorf("bad user/ident specified: %s", conf.Nick)}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -252,6 +299,15 @@ func New(config Config) *Client {
|
||||
initTime: time.Now(),
|
||||
}
|
||||
|
||||
c.IRCd = Server{
|
||||
Network: atomic.Value{},
|
||||
Version: "",
|
||||
UserCount: 0,
|
||||
MaxUserCount: 0,
|
||||
}
|
||||
|
||||
c.IRCd.Network.Store("")
|
||||
|
||||
c.Cmd = &Commands{c: c}
|
||||
|
||||
if c.Config.PingDelay >= 0 && c.Config.PingDelay < (20*time.Second) {
|
||||
@ -265,7 +321,7 @@ func New(config Config) *Client {
|
||||
if envDebug {
|
||||
c.debug = log.New(os.Stderr, "debug:", log.Ltime|log.Lshortfile)
|
||||
} else {
|
||||
c.debug = log.New(ioutil.Discard, "", 0)
|
||||
c.debug = log.New(io.Discard, "", 0)
|
||||
}
|
||||
} else {
|
||||
if envDebug {
|
||||
@ -277,18 +333,26 @@ func New(config Config) *Client {
|
||||
c.debug.Print("initializing debugging")
|
||||
}
|
||||
|
||||
envDisableSTS, _ := strconv.ParseBool((os.Getenv("GIRC_DISABLE_STS")))
|
||||
envDisableSTS, _ := strconv.ParseBool(os.Getenv("GIRC_DISABLE_STS"))
|
||||
if envDisableSTS {
|
||||
c.Config.DisableSTS = envDisableSTS
|
||||
}
|
||||
|
||||
// Setup the caller.
|
||||
c.Handlers = newCaller(c.debug)
|
||||
c.Handlers = newCaller(c, c.debug)
|
||||
|
||||
// Give ourselves a new state.
|
||||
c.state = &state{}
|
||||
c.state = &state{
|
||||
channels: cmap.New[*Channel](),
|
||||
users: cmap.New[*User](),
|
||||
enabledCap: cmap.New[map[string]string](),
|
||||
serverOptions: cmap.New[string](),
|
||||
}
|
||||
c.state.RWMutex = &sync.RWMutex{}
|
||||
c.state.reset(true)
|
||||
|
||||
c.state.client = c
|
||||
|
||||
// Register builtin handlers.
|
||||
c.registerBuiltins()
|
||||
|
||||
@ -313,16 +377,11 @@ func (c *Client) String() string {
|
||||
// connection wasn't established using TLS (see ErrConnNotTLS), or if the
|
||||
// client isn't connected.
|
||||
func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.conn == nil {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
defer c.conn.mu.RUnlock()
|
||||
|
||||
if !c.conn.connected {
|
||||
if !c.conn.connected.Load().(bool) {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
@ -343,12 +402,10 @@ var ErrConnNotTLS = errors.New("underlying connection is not tls")
|
||||
// safe to call multiple times. See Connect()'s documentation on how
|
||||
// handlers and goroutines are handled when disconnected from the server.
|
||||
func (c *Client) Close() {
|
||||
c.mu.RLock()
|
||||
if c.stop != nil {
|
||||
c.debug.Print("requesting client to stop")
|
||||
c.stop()
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Quit sends a QUIT message to the server with a given reason to close the
|
||||
@ -378,10 +435,12 @@ func (e *ErrEvent) Error() string {
|
||||
return e.Event.Last()
|
||||
}
|
||||
|
||||
func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
func (c *Client) execLoop(ctx context.Context, errs chan error, working *int32) {
|
||||
c.debug.Print("starting execLoop")
|
||||
defer c.debug.Print("closing execLoop")
|
||||
|
||||
defer atomic.AddInt32(working, -1)
|
||||
|
||||
var event *Event
|
||||
|
||||
for {
|
||||
@ -401,7 +460,6 @@ func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
}
|
||||
|
||||
done:
|
||||
wg.Done()
|
||||
return
|
||||
case event = <-c.rx:
|
||||
if event != nil && event.Command == ERROR {
|
||||
@ -432,9 +490,7 @@ func (c *Client) DisableTracking() {
|
||||
c.Config.disableTracking = true
|
||||
c.Handlers.clearInternal()
|
||||
|
||||
c.state.Lock()
|
||||
c.state.channels = nil
|
||||
c.state.Unlock()
|
||||
c.state.channels.Clear()
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
|
||||
c.registerBuiltins()
|
||||
@ -442,9 +498,6 @@ func (c *Client) DisableTracking() {
|
||||
|
||||
// Server returns the string representation of host+port pair for the connection.
|
||||
func (c *Client) Server() string {
|
||||
c.state.Lock()
|
||||
defer c.state.Lock()
|
||||
|
||||
return c.server()
|
||||
}
|
||||
|
||||
@ -465,16 +518,12 @@ func (c *Client) Lifetime() time.Duration {
|
||||
|
||||
// Uptime is the time at which the client successfully connected to the
|
||||
// server.
|
||||
func (c *Client) Uptime() (up *time.Time, err error) {
|
||||
func (c *Client) Uptime() (up time.Time, err error) {
|
||||
if !c.IsConnected() {
|
||||
return nil, ErrNotConnected
|
||||
return time.Now(), ErrNotConnected
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
up = c.conn.connTime
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
up = c.conn.connTime.Load().(time.Time)
|
||||
|
||||
return up, nil
|
||||
}
|
||||
@ -486,43 +535,40 @@ func (c *Client) ConnSince() (since *time.Duration, err error) {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
timeSince := time.Since(*c.conn.connTime)
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
timeSince := time.Since(c.conn.connTime.Load().(time.Time))
|
||||
|
||||
return &timeSince, nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if the client is connected to the server.
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.mu.RLock()
|
||||
if c.conn == nil {
|
||||
c.mu.RUnlock()
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
if c.conn == nil {
|
||||
return false
|
||||
}
|
||||
if c.conn.connected.Load() == nil {
|
||||
c.conn.connected.Store(false)
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
connected := c.conn.connected
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
return connected
|
||||
return c.conn.connected.Load().(bool)
|
||||
}
|
||||
|
||||
// GetNick returns the current nickname of the active connection. Panics if
|
||||
// tracking is disabled.
|
||||
func (c *Client) GetNick() string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
c.panicIfNotTracking()
|
||||
n := c.state.nick.Load().(string)
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
if c.state.nick == "" {
|
||||
if len(n) < 1 {
|
||||
return c.Config.Nick
|
||||
}
|
||||
return c.state.nick
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// GetID returns an RFC1459 compliant version of the current nickname. Panics
|
||||
@ -537,13 +583,10 @@ func (c *Client) GetID() string {
|
||||
func (c *Client) GetIdent() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
if c.state.ident == "" {
|
||||
if c.state.ident.Load().(string) == "" {
|
||||
return c.Config.User
|
||||
}
|
||||
return c.state.ident
|
||||
return c.state.ident.Load().(string)
|
||||
}
|
||||
|
||||
// GetHost returns the current host of the active connection. Panics if
|
||||
@ -552,9 +595,8 @@ func (c *Client) GetIdent() string {
|
||||
func (c *Client) GetHost() (host string) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
host = c.state.host
|
||||
c.state.RUnlock()
|
||||
host = c.state.host.Load().(string)
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
@ -563,12 +605,15 @@ func (c *Client) GetHost() (host string) {
|
||||
func (c *Client) ChannelList() []string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
channels := make([]string, 0, len(c.state.channels))
|
||||
for channel := range c.state.channels {
|
||||
channels = append(channels, c.state.channels[channel].Name)
|
||||
channels := make([]string, 0, len(c.state.channels.Keys()))
|
||||
for channel := range c.state.channels.IterBuffered() {
|
||||
chn := channel.Val
|
||||
if !chn.UserIn(c.GetNick()) {
|
||||
continue
|
||||
}
|
||||
channels = append(channels, chn.Name)
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
sort.Strings(channels)
|
||||
return channels
|
||||
}
|
||||
@ -578,12 +623,11 @@ func (c *Client) ChannelList() []string {
|
||||
func (c *Client) Channels() []*Channel {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
channels := make([]*Channel, 0, len(c.state.channels))
|
||||
for channel := range c.state.channels {
|
||||
channels = append(channels, c.state.channels[channel].Copy())
|
||||
channels := make([]*Channel, 0, c.state.channels.Count())
|
||||
for channel := range c.state.channels.IterBuffered() {
|
||||
chn := channel.Val
|
||||
channels = append(channels, chn.Copy())
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
sort.Slice(channels, func(i, j int) bool {
|
||||
return channels[i].Name < channels[j].Name
|
||||
@ -596,12 +640,15 @@ func (c *Client) Channels() []*Channel {
|
||||
func (c *Client) UserList() []string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
users := make([]string, 0, len(c.state.users))
|
||||
for user := range c.state.users {
|
||||
users = append(users, c.state.users[user].Nick)
|
||||
users := make([]string, 0, c.state.users.Count())
|
||||
for user := range c.state.users.IterBuffered() {
|
||||
usr := user.Val
|
||||
if usr.Stale {
|
||||
continue
|
||||
}
|
||||
users = append(users, usr.Nick.Load().(string))
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
sort.Strings(users)
|
||||
return users
|
||||
}
|
||||
@ -611,15 +658,14 @@ func (c *Client) UserList() []string {
|
||||
func (c *Client) Users() []*User {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
users := make([]*User, 0, len(c.state.users))
|
||||
for user := range c.state.users {
|
||||
users = append(users, c.state.users[user].Copy())
|
||||
users := make([]*User, 0, c.state.users.Count())
|
||||
for user := range c.state.users.IterBuffered() {
|
||||
usr := user.Val
|
||||
users = append(users, usr.Copy())
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].Nick < users[j].Nick
|
||||
return users[i].Nick.Load().(string) < users[j].Nick.Load().(string)
|
||||
})
|
||||
return users
|
||||
}
|
||||
@ -632,9 +678,8 @@ func (c *Client) LookupChannel(name string) (channel *Channel) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.state.RLock()
|
||||
channel = c.state.lookupChannel(name).Copy()
|
||||
c.state.RUnlock()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
@ -646,45 +691,71 @@ func (c *Client) LookupUser(nick string) (user *User) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.state.RLock()
|
||||
user = c.state.lookupUser(nick).Copy()
|
||||
c.state.RUnlock()
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// IsInChannel returns true if the client is in channel. Panics if tracking
|
||||
// is disabled.
|
||||
// TODO: make sure this still works.
|
||||
func (c *Client) IsInChannel(channel string) (in bool) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
_, in = c.state.channels[ToRFC1459(channel)]
|
||||
c.state.RUnlock()
|
||||
_, in = c.state.channels.Get(ToRFC1459(channel))
|
||||
return in
|
||||
}
|
||||
|
||||
// GetServerOption retrieves a server capability setting that was retrieved
|
||||
// GetServerOpt 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) {
|
||||
// nickLen, success := GetServerOpt("MAXNICKLEN")
|
||||
func (c *Client) GetServerOpt(key string) (result string, ok bool) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
result, ok = c.state.serverOptions[key]
|
||||
c.state.RUnlock()
|
||||
result, ok = c.state.serverOptions.Get(key)
|
||||
if !ok {
|
||||
return "", ok
|
||||
}
|
||||
|
||||
if len(result) > 0 {
|
||||
ok = true
|
||||
}
|
||||
|
||||
return result, ok
|
||||
}
|
||||
|
||||
// GetServerOptions retrieves all of a server's capability settings that were retrieved
|
||||
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
|
||||
func (c *Client) GetServerOptions() []byte {
|
||||
o := make(map[string]string)
|
||||
for opt := range c.state.serverOptions.IterBuffered() {
|
||||
o[opt.Key] = opt.Val
|
||||
}
|
||||
jcytes, _ := json.Marshal(o)
|
||||
return jcytes
|
||||
}
|
||||
|
||||
// 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()
|
||||
var ok bool
|
||||
|
||||
if len(c.state.network.Load().(string)) > 0 {
|
||||
return c.state.network.Load().(string)
|
||||
}
|
||||
|
||||
name, ok = c.GetServerOpt("NETWORK")
|
||||
if !ok {
|
||||
return c.IRCd.Network.Load().(string)
|
||||
}
|
||||
|
||||
if len(name) < 1 && len(c.IRCd.Network.Load().(string)) > 1 {
|
||||
name = c.IRCd.Network.Load().(string)
|
||||
}
|
||||
|
||||
name, _ = c.GetServerOption("NETWORK")
|
||||
return name
|
||||
}
|
||||
|
||||
@ -695,7 +766,7 @@ func (c *Client) NetworkName() (name string) {
|
||||
func (c *Client) ServerVersion() (version string) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
version, _ = c.GetServerOption("VERSION")
|
||||
version, _ = c.GetServerOpt("VERSION")
|
||||
return version
|
||||
}
|
||||
|
||||
@ -704,21 +775,14 @@ func (c *Client) ServerVersion() (version string) {
|
||||
func (c *Client) ServerMOTD() (motd string) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
motd = c.state.motd
|
||||
c.state.RUnlock()
|
||||
return motd
|
||||
return c.state.motd
|
||||
}
|
||||
|
||||
// Latency is the latency between the server and the client. This is measured
|
||||
// by determining the difference in time between when we ping the server, and
|
||||
// when we receive a pong.
|
||||
func (c *Client) Latency() (delta time.Duration) {
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
delta = c.conn.lastPong.Sub(c.conn.lastPing)
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
delta = c.conn.lastPong.Load().(time.Time).Sub(c.conn.lastPing.Load().(time.Time))
|
||||
|
||||
if delta < 0 {
|
||||
return 0
|
||||
@ -739,15 +803,13 @@ func (c *Client) HasCapability(name string) (has bool) {
|
||||
|
||||
name = strings.ToLower(name)
|
||||
|
||||
c.state.RLock()
|
||||
for key := range c.state.enabledCap {
|
||||
key = strings.ToLower(key)
|
||||
for capab := range c.state.enabledCap.IterBuffered() {
|
||||
key := strings.ToLower(capab.Key)
|
||||
if key == name {
|
||||
has = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return has
|
||||
}
|
||||
@ -756,6 +818,9 @@ func (c *Client) HasCapability(name string) (has bool) {
|
||||
// disabled. Adds useful info like what function specifically, and where it
|
||||
// was called from.
|
||||
func (c *Client) panicIfNotTracking() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if !c.Config.disableTracking {
|
||||
return
|
||||
}
|
||||
@ -783,8 +848,10 @@ func (c *Client) debugLogEvent(e *Event, dropped bool) {
|
||||
}
|
||||
|
||||
if c.Config.Out != nil {
|
||||
|
||||
if pretty, ok := e.Pretty(); ok {
|
||||
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||
|
||||
_, _ = fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,19 +19,16 @@ func TestDisableTracking(t *testing.T) {
|
||||
Name: "Testing123",
|
||||
})
|
||||
|
||||
if len(client.Handlers.internal) < 1 {
|
||||
if client.Handlers.internal.len() < 1 {
|
||||
t.Fatal("Client.Handlers empty, though just initialized")
|
||||
}
|
||||
|
||||
client.DisableTracking()
|
||||
if _, ok := client.Handlers.internal[CAP]; ok {
|
||||
if _, ok := client.Handlers.internal.cm.Get(CAP); ok {
|
||||
t.Fatal("Client.Handlers contains capability tracking handlers, though disabled")
|
||||
}
|
||||
|
||||
client.state.Lock()
|
||||
defer client.state.Unlock()
|
||||
|
||||
if client.state.channels != nil {
|
||||
if len(client.state.channels.Keys()) > 0 {
|
||||
t.Fatal("Client.DisableTracking() called but channel state still exists")
|
||||
}
|
||||
}
|
||||
@ -96,14 +93,24 @@ func TestClientLifetime(t *testing.T) {
|
||||
|
||||
func TestClientUptime(t *testing.T) {
|
||||
c, conn, server := genMockConn()
|
||||
defer conn.Close()
|
||||
defer server.Close()
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Errorf("failed to close connection: %s", err)
|
||||
}
|
||||
if err := server.Close(); err != nil {
|
||||
t.Errorf("failed to close server: %s", err)
|
||||
}
|
||||
}()
|
||||
go mockReadBuffer(conn)
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
c.Handlers.Add(INITIALIZED, func(c *Client, e Event) { close(done) })
|
||||
|
||||
go c.MockConnect(server)
|
||||
go func() {
|
||||
if err := c.MockConnect(server); err != nil {
|
||||
t.Errorf("failed to connect: %s", err)
|
||||
}
|
||||
}()
|
||||
defer c.Close()
|
||||
|
||||
select {
|
||||
@ -117,7 +124,7 @@ func TestClientUptime(t *testing.T) {
|
||||
t.Fatalf("Client.Uptime() = %s, wanted time", err)
|
||||
}
|
||||
|
||||
since := time.Since(*uptime)
|
||||
since := time.Since(uptime)
|
||||
connsince, err := c.ConnSince()
|
||||
if err != nil {
|
||||
t.Fatalf("Client.ConnSince() = %s, wanted time", err)
|
||||
@ -141,14 +148,24 @@ func TestClientUptime(t *testing.T) {
|
||||
|
||||
func TestClientGet(t *testing.T) {
|
||||
c, conn, server := genMockConn()
|
||||
defer conn.Close()
|
||||
defer server.Close()
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Errorf("failed to close connection: %s", err)
|
||||
}
|
||||
if err := server.Close(); err != nil {
|
||||
t.Errorf("failed to close server: %s", err)
|
||||
}
|
||||
}()
|
||||
go mockReadBuffer(conn)
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
c.Handlers.Add(INITIALIZED, func(c *Client, e Event) { close(done) })
|
||||
|
||||
go c.MockConnect(server)
|
||||
go func() {
|
||||
if err := c.MockConnect(server); err != nil {
|
||||
t.Errorf("failed to connect: %s", err)
|
||||
}
|
||||
}()
|
||||
defer c.Close()
|
||||
|
||||
select {
|
||||
@ -172,8 +189,14 @@ func TestClientGet(t *testing.T) {
|
||||
|
||||
func TestClientClose(t *testing.T) {
|
||||
c, conn, server := genMockConn()
|
||||
defer server.Close()
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Errorf("failed to close connection: %s", err)
|
||||
}
|
||||
if err := server.Close(); err != nil {
|
||||
t.Errorf("failed to close server: %s", err)
|
||||
}
|
||||
}()
|
||||
go mockReadBuffer(conn)
|
||||
|
||||
errchan := make(chan error, 1)
|
||||
|
@ -1,199 +0,0 @@
|
||||
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
|
||||
RawArgs 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.Last())
|
||||
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,
|
||||
RawArgs: parsed[2],
|
||||
}
|
||||
|
||||
go cmd.Fn(client, in)
|
||||
}
|
230
codebook.go
Normal file
230
codebook.go
Normal file
@ -0,0 +1,230 @@
|
||||
package girc
|
||||
|
||||
/*
|
||||
IRCNumToStr takes in a numeric IRC code and returns the relevant girc name.
|
||||
IRCNumToStr accepts a string because that's how we tend to receive the codes.
|
||||
*/
|
||||
//goland:noinspection GoUnusedExportedFunction
|
||||
func IRCNumToStr(code string) string {
|
||||
if _, ok := noTranslate[code]; ok {
|
||||
return code
|
||||
}
|
||||
if result, ok := IRCCodes[code]; ok {
|
||||
return result
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var noTranslate = map[string]uint8{"JOIN": 1, "INVITE": 1, "NOTICE": 1, "CAP": 1, "MODE": 1, "QUIT": 1, "PRIVMSG": 1, "NICK": 1, "FAIL": 1}
|
||||
|
||||
// IRCCodes is just a map form of our constants for quick lookup purposes.
|
||||
var IRCCodes = map[string]string{
|
||||
"001": "RPL_WELCOME",
|
||||
"002": "RPL_YOURHOST",
|
||||
"003": "RPL_CREATED",
|
||||
"004": "RPL_MYINFO",
|
||||
"005": "RPL_ISUPPORT",
|
||||
"302": "RPL_USERHOST",
|
||||
"303": "RPL_ISON",
|
||||
"301": "RPL_AWAY",
|
||||
"305": "RPL_UNAWAY",
|
||||
"306": "RPL_NOWAWAY",
|
||||
"311": "RPL_WHOISUSER",
|
||||
"312": "RPL_WHOISSERVER",
|
||||
"313": "RPL_WHOISOPERATOR",
|
||||
"317": "RPL_WHOISIDLE",
|
||||
"318": "RPL_ENDOFWHOIS",
|
||||
"319": "RPL_WHOISCHANNELS",
|
||||
"314": "RPL_WHOWASUSER",
|
||||
"369": "RPL_ENDOFWHOWAS",
|
||||
"321": "RPL_LISTSTART",
|
||||
"322": "RPL_LIST",
|
||||
"323": "RPL_LISTEND",
|
||||
"325": "RPL_UNIQOPIS",
|
||||
"324": "RPL_CHANNELMODEIS",
|
||||
"329": "RPL_CREATIONTIME",
|
||||
"330": "RPL_WHOISAUTHNAME",
|
||||
"331": "RPL_NOTOPIC",
|
||||
"332": "RPL_TOPIC",
|
||||
"341": "RPL_INVITING",
|
||||
"342": "RPL_SUMMONING",
|
||||
"346": "RPL_INVITELIST",
|
||||
"347": "RPL_ENDOFINVITELIST",
|
||||
"348": "RPL_EXCEPTLIST",
|
||||
"349": "RPL_ENDOFEXCEPTLIST",
|
||||
"351": "RPL_VERSION",
|
||||
"352": "RPL_WHOREPLY",
|
||||
"315": "RPL_ENDOFWHO",
|
||||
"353": "RPL_NAMREPLY",
|
||||
"366": "RPL_ENDOFNAMES",
|
||||
"364": "RPL_LINKS",
|
||||
"365": "RPL_ENDOFLINKS",
|
||||
"367": "RPL_BANLIST",
|
||||
"368": "RPL_ENDOFBANLIST",
|
||||
"371": "RPL_INFO",
|
||||
"374": "RPL_ENDOFINFO",
|
||||
"375": "RPL_MOTDSTART",
|
||||
"372": "RPL_MOTD",
|
||||
"376": "RPL_ENDOFMOTD",
|
||||
"381": "RPL_YOUREOPER",
|
||||
"382": "RPL_REHASHING",
|
||||
"383": "RPL_YOURESERVICE",
|
||||
"391": "RPL_TIME",
|
||||
"392": "RPL_USERSSTART",
|
||||
"393": "RPL_USERS",
|
||||
"394": "RPL_ENDOFUSERS",
|
||||
"395": "RPL_NOUSERS",
|
||||
"200": "RPL_TRACELINK",
|
||||
"201": "RPL_TRACECONNECTING",
|
||||
"202": "RPL_TRACEHANDSHAKE",
|
||||
"203": "RPL_TRACEUNKNOWN",
|
||||
"204": "RPL_TRACEOPERATOR",
|
||||
"205": "RPL_TRACEUSER",
|
||||
"206": "RPL_TRACESERVER",
|
||||
"207": "RPL_TRACESERVICE",
|
||||
"208": "RPL_TRACENEWTYPE",
|
||||
"209": "RPL_TRACECLASS",
|
||||
"210": "RPL_TRACERECONNECT",
|
||||
"261": "RPL_TRACELOG",
|
||||
"262": "RPL_TRACEEND",
|
||||
"211": "RPL_STATSLINKINFO",
|
||||
"212": "RPL_STATSCOMMANDS",
|
||||
"219": "RPL_ENDOFSTATS",
|
||||
"242": "RPL_STATSUPTIME",
|
||||
"243": "RPL_STATSOLINE",
|
||||
"221": "RPL_UMODEIS",
|
||||
"234": "RPL_SERVLIST",
|
||||
"235": "RPL_SERVLISTEND",
|
||||
"251": "RPL_LUSERCLIENT",
|
||||
"252": "RPL_LUSEROP",
|
||||
"253": "RPL_LUSERUNKNOWN",
|
||||
"254": "RPL_LUSERCHANNELS",
|
||||
"255": "RPL_LUSERME",
|
||||
"256": "RPL_ADMINME",
|
||||
"257": "RPL_ADMINLOC1",
|
||||
"258": "RPL_ADMINLOC2",
|
||||
"259": "RPL_ADMINEMAIL",
|
||||
"263": "RPL_TRYAGAIN",
|
||||
"400": "ERR_ALREADYOPER",
|
||||
"401": "ERR_NOSUCHNICK",
|
||||
"402": "ERR_NOSUCHSERVER",
|
||||
"403": "ERR_NOSUCHCHANNEL",
|
||||
"404": "ERR_CANNOTSENDTOCHAN",
|
||||
"405": "ERR_TOOMANYCHANNELS",
|
||||
"406": "ERR_WASNOSUCHNICK",
|
||||
"407": "ERR_TOOMANYTARGETS",
|
||||
"408": "ERR_NOSUCHSERVICE",
|
||||
"409": "ERR_NOORIGIN",
|
||||
"411": "ERR_NORECIPIENT",
|
||||
"412": "ERR_NOTEXTTOSEND",
|
||||
"413": "ERR_NOTOPLEVEL",
|
||||
"414": "ERR_WILDTOPLEVEL",
|
||||
"415": "ERR_BADMASK",
|
||||
"417": "ERR_INPUTTOOLONG",
|
||||
"421": "ERR_UNKNOWNCOMMAND",
|
||||
"422": "ERR_NOMOTD",
|
||||
"423": "ERR_NOADMININFO",
|
||||
"424": "ERR_FILEERROR",
|
||||
"431": "ERR_NONICKNAMEGIVEN",
|
||||
"432": "ERR_ERRONEUSNICKNAME",
|
||||
"433": "ERR_NICKNAMEINUSE",
|
||||
"436": "ERR_NICKCOLLISION",
|
||||
"437": "ERR_UNAVAILRESOURCE",
|
||||
"441": "ERR_USERNOTINCHANNEL",
|
||||
"442": "ERR_NOTONCHANNEL",
|
||||
"443": "ERR_USERONCHANNEL",
|
||||
"444": "ERR_NOLOGIN",
|
||||
"445": "ERR_SUMMONDISABLED",
|
||||
"446": "ERR_USERSDISABLED",
|
||||
"451": "ERR_NOTREGISTERED",
|
||||
"461": "ERR_NEEDMOREPARAMS",
|
||||
"462": "ERR_ALREADYREGISTRED",
|
||||
"463": "ERR_NOPERMFORHOST",
|
||||
"464": "ERR_PASSWDMISMATCH",
|
||||
"465": "ERR_YOUREBANNEDCREEP",
|
||||
"466": "ERR_YOUWILLBEBANNED",
|
||||
"467": "ERR_KEYSET",
|
||||
"471": "ERR_CHANNELISFULL",
|
||||
"472": "ERR_UNKNOWNMODE",
|
||||
"473": "ERR_INVITEONLYCHAN",
|
||||
"474": "ERR_BANNEDFROMCHAN",
|
||||
"475": "ERR_BADCHANNELKEY",
|
||||
"476": "ERR_BADCHANMASK",
|
||||
"477": "ERR_NOCHANMODES",
|
||||
"478": "ERR_BANLISTFULL",
|
||||
"481": "ERR_NOPRIVILEGES",
|
||||
"482": "ERR_CHANOPRIVSNEEDED",
|
||||
"483": "ERR_CANTKILLSERVER",
|
||||
"484": "ERR_RESTRICTED",
|
||||
"485": "ERR_UNIQOPPRIVSNEEDED",
|
||||
"491": "ERR_NOOPERHOST",
|
||||
"501": "ERR_UMODEUNKNOWNFLAG",
|
||||
"502": "ERR_USERSDONTMATCH",
|
||||
"900": "RPL_LOGGEDIN",
|
||||
"901": "RPL_LOGGEDOUT",
|
||||
"902": "RPL_NICKLOCKED",
|
||||
"903": "RPL_SASLSUCCESS",
|
||||
"904": "ERR_SASLFAIL",
|
||||
"905": "ERR_SASLTOOLONG",
|
||||
"906": "ERR_SASLABORTED",
|
||||
"907": "ERR_SASLALREADY",
|
||||
"908": "RPL_SASLMECHS",
|
||||
"670": "RPL_STARTTLS",
|
||||
"691": "ERR_STARTTLS",
|
||||
"730": "RPL_MONONLINE",
|
||||
"731": "RPL_MONOFFLINE",
|
||||
"732": "RPL_MONLIST",
|
||||
"733": "RPL_ENDOFMONLIST",
|
||||
"734": "ERR_MONLISTFULL",
|
||||
"213": "RPL_STATSCLINE",
|
||||
"214": "RPL_STATSNLINE",
|
||||
"215": "RPL_STATSILINE",
|
||||
"216": "RPL_STATSKLINE",
|
||||
"217": "RPL_STATSQLINE",
|
||||
"218": "RPL_STATSYLINE",
|
||||
"231": "RPL_SERVICEINFO",
|
||||
"232": "RPL_ENDOFSERVICES",
|
||||
"233": "RPL_SERVICE",
|
||||
"240": "RPL_STATSVLINE",
|
||||
"241": "RPL_STATSLLINE",
|
||||
"244": "RPL_STATSHLINE",
|
||||
"245": "RPL_STATSSLINE",
|
||||
"246": "RPL_STATSPING",
|
||||
"247": "RPL_STATSBLINE",
|
||||
"250": "RPL_STATSDLINE",
|
||||
"300": "RPL_NONE",
|
||||
"316": "RPL_WHOISCHANOP",
|
||||
"361": "RPL_KILLDONE",
|
||||
"362": "RPL_CLOSING",
|
||||
"363": "RPL_CLOSEEND",
|
||||
"373": "RPL_INFOSTART",
|
||||
"384": "RPL_MYPORTIS",
|
||||
"492": "ERR_NOSERVICEHOST",
|
||||
"416": "ERR_TOOMANYMATCHES",
|
||||
"266": "RPL_GLOBALUSERS",
|
||||
"265": "RPL_LOCALUSERS",
|
||||
"333": "RPL_TOPICWHOTIME",
|
||||
"354": "RPL_WHOSPCRPL",
|
||||
"671": "RPL_WHOISTLS",
|
||||
|
||||
"CLIENT_STATE_UPDATED": "UPDATE_STATE",
|
||||
"CLIENT_GENERAL_UPDATED": "UPDATE_GENERAL",
|
||||
"*": "ALL_EVENTS",
|
||||
"CLIENT_CONNECTED": "CONNECTED",
|
||||
"CLIENT_INIT": "INITIALIZED",
|
||||
"CLIENT_DISCONNECTED": "DISCONNECTED",
|
||||
"CLIENT_CLOSED": "CLOSED",
|
||||
"STS_UPGRADE_INIT": "STS_UPGRADE_INIT",
|
||||
"STS_ERR_FALLBACK": "STS_ERR_FALLBACK",
|
||||
|
||||
"ACTION": "CTCP_ACTION",
|
||||
"PING": "CTCP_PING",
|
||||
"PONG": "CTCP_PONG",
|
||||
"VERSION": "CTCP_VERSION",
|
||||
"USERINFO": "CTCP_USERINFO",
|
||||
"CLIENTINFO": "CTCP_CLIENTINFO",
|
||||
"SOURCE": "CTCP_SOURCE",
|
||||
"TIME": "CTCP_TIME",
|
||||
"FINGER": "CTCP_FINGER",
|
||||
"ERRMSG": "CTCP_ERRMSG",
|
||||
}
|
107
commands.go
107
commands.go
@ -36,7 +36,7 @@ func (cmd *Commands) Join(channels ...string) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(buffer) == 0 {
|
||||
if buffer == "" {
|
||||
buffer = channels[i]
|
||||
} else {
|
||||
buffer += "," + channels[i]
|
||||
@ -111,7 +111,8 @@ func (cmd *Commands) Message(target, message string) {
|
||||
// Messagef sends a formated PRIVMSG to target (either channel, service, or
|
||||
// user).
|
||||
func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
|
||||
cmd.Message(target, fmt.Sprintf(format, a...))
|
||||
message := fmt.Sprintf(format, a...)
|
||||
cmd.Message(target, Fmt(message))
|
||||
}
|
||||
|
||||
// ErrInvalidSource is returned when a method needs to know the origin of an
|
||||
@ -119,52 +120,83 @@ func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
|
||||
// server.)
|
||||
var ErrInvalidSource = errors.New("event has nil or invalid source address")
|
||||
|
||||
// ErrDontKnowUser is returned when a method needs to know the origin of an event,
|
||||
var ErrDontKnowUser = errors.New("failed to lookup target user")
|
||||
|
||||
// Reply sends a reply to channel or user, based on where the supplied event
|
||||
// originated from. See also ReplyTo(). Panics if the incoming event has no
|
||||
// source.
|
||||
func (cmd *Commands) Reply(event Event, message string) {
|
||||
func (cmd *Commands) Reply(event Event, message string) error {
|
||||
if event.Source == nil {
|
||||
panic(ErrInvalidSource)
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Message(event.Params[0], message)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.Message(event.Source.Name, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplyKick kicks the source of the event from the channel where the event originated
|
||||
func (cmd *Commands) ReplyKick(event Event, reason string) error {
|
||||
if event.Source == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Kick(event.Params[0], event.Source.Name, reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplyBan kicks the source of the event from the channel where the event originated.
|
||||
// Additionally, if a reason is provided, it will send a message to the channel.
|
||||
func (cmd *Commands) ReplyBan(event Event, reason string) (err error) {
|
||||
if event.Source == nil {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
if reason != "" {
|
||||
err = cmd.Replyf(event, "{red}{b}[BAN] {r}%s", reason)
|
||||
}
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Ban(event.Params[0], fmt.Sprintf("*!%s@%s", event.Source.Ident, event.Source.Host))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Replyf sends a reply to channel or user with a format string, based on
|
||||
// where the supplied event originated from. See also ReplyTof(). Panics if
|
||||
// the incoming event has no source.
|
||||
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) {
|
||||
cmd.Reply(event, fmt.Sprintf(format, a...))
|
||||
// Formatted means both in the sense of Sprintf as well as girc style macros.
|
||||
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
|
||||
message := fmt.Sprintf(format, a...)
|
||||
return cmd.Reply(event, Fmt(message))
|
||||
}
|
||||
|
||||
// ReplyTo sends a reply to a channel or user, based on where the supplied
|
||||
// event originated from. ReplyTo(), when originating from a channel will
|
||||
// default to replying with "<user>, <message>". See also Reply(). Panics if
|
||||
// the incoming event has no source.
|
||||
func (cmd *Commands) ReplyTo(event Event, message string) {
|
||||
func (cmd *Commands) ReplyTo(event Event, message string) error {
|
||||
if event.Source == nil {
|
||||
panic(ErrInvalidSource)
|
||||
return ErrInvalidSource
|
||||
}
|
||||
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Message(event.Params[0], event.Source.Name+", "+message)
|
||||
return
|
||||
} else {
|
||||
cmd.Message(event.Source.Name, message)
|
||||
}
|
||||
|
||||
cmd.Message(event.Source.Name, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplyTof sends a reply to a channel or user with a format string, based
|
||||
// on where the supplied event originated from. ReplyTo(), when originating
|
||||
// from a channel will default to replying with "<user>, <message>". See
|
||||
// also Replyf(). Panics if the incoming event has no source.
|
||||
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
|
||||
cmd.ReplyTo(event, fmt.Sprintf(format, a...))
|
||||
// Formatted means both in the sense of Sprintf as well as girc style macros.
|
||||
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
|
||||
message := fmt.Sprintf(format, a...)
|
||||
return cmd.ReplyTo(event, Fmt(message))
|
||||
}
|
||||
|
||||
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
|
||||
@ -188,9 +220,9 @@ func (cmd *Commands) Notice(target, message string) {
|
||||
}
|
||||
|
||||
// Noticef sends a formated NOTICE to target (either channel, service, or
|
||||
// user).
|
||||
// user). Formatted means both in the sense of Sprintf as well as girc styling codes.
|
||||
func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
|
||||
cmd.Notice(target, fmt.Sprintf(format, a...))
|
||||
cmd.Notice(target, Fmt(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// SendRaw sends a raw string (or multiple) to the server, without carriage
|
||||
@ -212,9 +244,9 @@ func (cmd *Commands) SendRaw(raw ...string) error {
|
||||
}
|
||||
|
||||
// SendRawf sends a formated string back to the server, without carriage
|
||||
// returns or newlines.
|
||||
// returns or newlines. Formatted means both in the sense of Sprintf as well as girc style macros.
|
||||
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
|
||||
return cmd.SendRaw(fmt.Sprintf(format, a...))
|
||||
return cmd.SendRaw(Fmt(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// Topic sets the topic of channel to message. Does not verify the length
|
||||
@ -223,13 +255,19 @@ func (cmd *Commands) Topic(channel, message string) {
|
||||
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, message}})
|
||||
}
|
||||
|
||||
// Topicf sets a formatted topic command to the channel. Does not verify the length
|
||||
// of the topic. Formatted means both in the sense of Sprintf as well as girc style macros.
|
||||
func (cmd *Commands) Topicf(channel, format string, a ...interface{}) {
|
||||
message := fmt.Sprintf(format, a...)
|
||||
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, Fmt(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.
|
||||
// sends "%tcuhnr,1" per default.
|
||||
func (cmd *Commands) Who(users ...string) {
|
||||
for i := 0; i < len(users); i++ {
|
||||
cmd.c.Send(&Event{Command: WHO, Params: []string{users[i], "%tcuhnr,2"}})
|
||||
cmd.c.Send(&Event{Command: WHO, Params: []string{users[i], "%tacuhnr,1"}})
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,6 +298,22 @@ func (cmd *Commands) Oper(user, pass string) {
|
||||
cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true})
|
||||
}
|
||||
|
||||
// KickBan 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. Afterwards it immediately sets +b on the mask given.
|
||||
// If no mask is given, it will set +b on *!~ident@host.
|
||||
//
|
||||
// Note: this command will return an error if it cannot track the user in order to determine ban mask.
|
||||
func (cmd *Commands) KickBan(channel, user, reason string) error {
|
||||
u := cmd.c.LookupUser(user)
|
||||
if u == nil {
|
||||
return ErrDontKnowUser
|
||||
}
|
||||
cmd.Kick(channel, user, reason)
|
||||
cmd.Ban(channel, fmt.Sprintf("*!%s@%s", u.Ident, u.Host))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -267,7 +321,6 @@ func (cmd *Commands) Kick(channel, user, reason string) {
|
||||
if reason != "" {
|
||||
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user, reason}})
|
||||
}
|
||||
|
||||
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}})
|
||||
}
|
||||
|
||||
@ -340,7 +393,7 @@ func (cmd *Commands) List(channels ...string) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(buffer) == 0 {
|
||||
if buffer == "" {
|
||||
buffer = channels[i]
|
||||
} else {
|
||||
buffer += "," + channels[i]
|
||||
@ -356,7 +409,7 @@ func (cmd *Commands) List(channels ...string) {
|
||||
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
||||
// you want back.
|
||||
func (cmd *Commands) Whowas(user string, amount int) {
|
||||
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(amount)}})
|
||||
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, fmt.Sprintf("%d", amount)}})
|
||||
}
|
||||
|
||||
// Monitor sends a MONITOR query to the server. The results of the query
|
||||
|
194
conn.go
194
conn.go
@ -10,8 +10,10 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.tcp.direct/kayos/common/pool"
|
||||
)
|
||||
|
||||
// Messages are delimited with CR and LF line endings, we're using the last
|
||||
@ -26,25 +28,23 @@ 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
|
||||
lastWrite atomic.Value
|
||||
// lastActive is the last time the client was interacting with the server,
|
||||
// excluding a few background commands (PING, PONG, WHO, etc).
|
||||
lastActive time.Time
|
||||
lastActive atomic.Value
|
||||
// writeDelay is used to keep track of rate limiting of events sent to
|
||||
// the server.
|
||||
writeDelay time.Duration
|
||||
writeDelay atomic.Value
|
||||
// connected is true if we're actively connected to a server.
|
||||
connected bool
|
||||
connected atomic.Value
|
||||
// connTime is the time at which the client has connected to a server.
|
||||
connTime *time.Time
|
||||
connTime atomic.Value
|
||||
// lastPing is the last time that we pinged the server.
|
||||
lastPing time.Time
|
||||
lastPing atomic.Value
|
||||
// lastPong is the last successful time that we pinged the server and
|
||||
// received a successful pong back.
|
||||
lastPong time.Time
|
||||
pingDelay time.Duration
|
||||
lastPong atomic.Value
|
||||
}
|
||||
|
||||
// Dialer is an interface implementation of net.Dialer. Use this if you would
|
||||
@ -57,6 +57,8 @@ type Dialer interface {
|
||||
Dial(network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
var strs = pool.NewStringFactory()
|
||||
|
||||
// newConn sets up and returns a new connection to the server.
|
||||
func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ircConn, error) {
|
||||
if err := conf.isValid(); err != nil {
|
||||
@ -71,7 +73,11 @@ func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ir
|
||||
|
||||
if conf.Bind != "" {
|
||||
var local *net.TCPAddr
|
||||
local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0")
|
||||
s := strs.Get()
|
||||
s.MustWriteString(conf.Bind)
|
||||
s.MustWriteString(":0")
|
||||
local, err = net.ResolveTCPAddr("tcp", s.String())
|
||||
strs.MustPut(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -112,25 +118,27 @@ func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ir
|
||||
conn = tlsConn
|
||||
}
|
||||
|
||||
ctime := time.Now()
|
||||
|
||||
c := &ircConn{
|
||||
sock: conn,
|
||||
connTime: &ctime,
|
||||
connected: true,
|
||||
connTime: atomic.Value{},
|
||||
connected: atomic.Value{},
|
||||
}
|
||||
c.connTime.Store(time.Now())
|
||||
c.connected.Store(true)
|
||||
|
||||
c.newReadWriter()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func newMockConn(conn net.Conn) *ircConn {
|
||||
ctime := time.Now()
|
||||
c := &ircConn{
|
||||
sock: conn,
|
||||
connTime: &ctime,
|
||||
connected: true,
|
||||
connTime: atomic.Value{},
|
||||
connected: atomic.Value{},
|
||||
}
|
||||
c.connTime.Store(time.Now())
|
||||
c.connected.Store(true)
|
||||
c.newReadWriter()
|
||||
|
||||
return c
|
||||
@ -143,6 +151,17 @@ type ErrParseEvent struct {
|
||||
|
||||
func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line }
|
||||
|
||||
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) decode() (event *Event, err error) {
|
||||
line, err := c.io.ReadString(delim)
|
||||
if err != nil {
|
||||
@ -156,17 +175,6 @@ func (c *ircConn) decode() (event *Event, err error) {
|
||||
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))
|
||||
}
|
||||
@ -262,9 +270,6 @@ func (c *Client) MockConnect(conn net.Conn) error {
|
||||
|
||||
func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
|
||||
startConn:
|
||||
// We want to be the only one handling connects/disconnects right now.
|
||||
c.mu.Lock()
|
||||
|
||||
if c.conn != nil {
|
||||
panic("use of connect more than once")
|
||||
}
|
||||
@ -276,7 +281,7 @@ startConn:
|
||||
|
||||
if mock == nil {
|
||||
// Validate info, and actually make the connection.
|
||||
c.debug.Printf("connecting to %s... (sts: %v, config-ssl: %v)", addr, c.state.sts.enabled(), c.Config.SSL)
|
||||
c.debug.Printf("(%s) connecting to %s... (sts: %v, config-ssl: %v)", c.Config.Nick, addr, c.state.sts.enabled(), c.Config.SSL)
|
||||
conn, err := newConn(c.Config, dialer, addr, &c.state.sts)
|
||||
if err != nil {
|
||||
if _, ok := err.(*ErrSTSUpgradeFailed); ok {
|
||||
@ -284,7 +289,6 @@ startConn:
|
||||
c.RunHandlers(&Event{Command: STS_ERR_FALLBACK})
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
@ -295,17 +299,16 @@ startConn:
|
||||
|
||||
var ctx context.Context
|
||||
ctx, c.stop = context.WithCancel(context.Background())
|
||||
c.mu.Unlock()
|
||||
|
||||
errs := make(chan error, 4)
|
||||
var wg sync.WaitGroup
|
||||
var working int32
|
||||
// 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)
|
||||
atomic.AddInt32(&working, 4)
|
||||
go c.execLoop(ctx, errs, &working)
|
||||
go c.readLoop(ctx, errs, &working)
|
||||
go c.sendLoop(ctx, errs, &working)
|
||||
go c.pingLoop(ctx, errs, &working)
|
||||
|
||||
// Passwords first.
|
||||
|
||||
@ -326,7 +329,9 @@ startConn:
|
||||
c.listCAP()
|
||||
|
||||
// Then nickname.
|
||||
c.state.RLock()
|
||||
c.write(&Event{Command: NICK, Params: []string{c.Config.Nick}})
|
||||
c.state.RUnlock()
|
||||
|
||||
// Then username and realname.
|
||||
if c.Config.Name == "" {
|
||||
@ -347,40 +352,41 @@ startConn:
|
||||
}
|
||||
c.RunHandlers(&Event{Command: CLOSED, Params: []string{addr}})
|
||||
case err := <-errs:
|
||||
c.debug.Printf("received error, beginning cleanup: %v", err)
|
||||
c.debug.Printf("(%s) received error, beginning cleanup: %v", c.Config.Nick, err)
|
||||
result = err
|
||||
}
|
||||
|
||||
// Make sure that the connection is closed if not already.
|
||||
c.mu.RLock()
|
||||
|
||||
if c.stop != nil {
|
||||
c.stop()
|
||||
}
|
||||
c.conn.mu.Lock()
|
||||
c.conn.connected = false
|
||||
|
||||
c.conn.connected.Store(false)
|
||||
_ = c.conn.Close()
|
||||
c.conn.mu.Unlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{addr}})
|
||||
|
||||
// Once we have our error/result, let all other functions know we're done.
|
||||
c.debug.Print("waiting for all routines to finish")
|
||||
|
||||
for {
|
||||
if atomic.LoadInt32(&working) <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all goroutines to finish.
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
// This helps ensure that the end user isn't improperly using the client
|
||||
// more than once. If they want to do this, they should be using multiple
|
||||
// clients, not multiple instances of Connect().
|
||||
c.mu.Lock()
|
||||
c.conn = nil
|
||||
|
||||
if result == nil {
|
||||
if c.state.sts.beginUpgrade {
|
||||
c.state.sts.beginUpgrade = false
|
||||
c.mu.Unlock()
|
||||
goto startConn
|
||||
}
|
||||
|
||||
@ -388,14 +394,13 @@ startConn:
|
||||
c.state.sts.persistenceReceived = time.Now()
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// readLoop sets a timeout of 300 seconds, and then attempts to read from the
|
||||
// IRC server. If there is an error, it calls Reconnect.
|
||||
func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
func (c *Client) readLoop(ctx context.Context, errs chan error, working *int32) {
|
||||
c.debug.Print("starting readLoop")
|
||||
defer c.debug.Print("closing readLoop")
|
||||
|
||||
@ -405,17 +410,19 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
atomic.AddInt32(working, -1)
|
||||
return
|
||||
default:
|
||||
_ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second))
|
||||
event, err = c.conn.decode()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
wg.Done()
|
||||
atomic.AddInt32(working, -1)
|
||||
return
|
||||
}
|
||||
|
||||
event.Network = c.NetworkName()
|
||||
|
||||
// Check if it's an echo-message.
|
||||
if !c.Config.disableTracking {
|
||||
event.Echo = (event.Command == PRIVMSG || event.Command == NOTICE) &&
|
||||
@ -432,21 +439,17 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
func (c *Client) Send(event *Event) {
|
||||
var delay time.Duration
|
||||
|
||||
if !c.Config.AllowFlood {
|
||||
c.mu.RLock()
|
||||
event.Network = c.NetworkName()
|
||||
|
||||
if !c.Config.AllowFlood {
|
||||
// Drop the event early as we're disconnected, this way we don't have to wait
|
||||
// the (potentially long) rate limit delay before dropping.
|
||||
if c.conn == nil {
|
||||
c.debugLogEvent(event, true)
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
c.conn.mu.Lock()
|
||||
delay = c.conn.rate(event.Len())
|
||||
c.conn.mu.Unlock()
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" &&
|
||||
@ -461,9 +464,6 @@ func (c *Client) Send(event *Event) {
|
||||
// write is the lower level function to write an event. It does not have a
|
||||
// write-delay when sending events.
|
||||
func (c *Client) write(event *Event) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.conn == nil {
|
||||
// Drop the event if disconnected.
|
||||
c.debugLogEvent(event, true)
|
||||
@ -477,21 +477,30 @@ func (c *Client) write(event *Event) {
|
||||
func (c *ircConn) rate(chars int) time.Duration {
|
||||
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
|
||||
|
||||
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
|
||||
c.writeDelay = 0
|
||||
if c.writeDelay.Load() == nil {
|
||||
c.writeDelay.Store(time.Duration(0))
|
||||
}
|
||||
wdelay := c.writeDelay.Load().(time.Duration)
|
||||
|
||||
lwrite := c.lastWrite.Load().(time.Time)
|
||||
|
||||
if wdelay += _time - time.Since(lwrite); wdelay < 0 {
|
||||
c.writeDelay.Store(time.Duration(0))
|
||||
}
|
||||
|
||||
if c.writeDelay > (8 * time.Second) {
|
||||
if c.writeDelay.Load().(time.Duration) > (8 * time.Second) {
|
||||
return _time
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
func (c *Client) sendLoop(ctx context.Context, errs chan error, working *int32) {
|
||||
c.debug.Print("starting sendLoop")
|
||||
defer c.debug.Print("closing sendLoop")
|
||||
|
||||
defer atomic.AddInt32(working, -1)
|
||||
|
||||
var err error
|
||||
|
||||
for {
|
||||
@ -500,15 +509,13 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
// Check if tags exist on the event. If they do, and message-tags
|
||||
// isn't a supported capability, remove them from the event.
|
||||
if event.Tags != nil {
|
||||
c.state.RLock()
|
||||
var in bool
|
||||
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||
if _, ok := c.state.enabledCap["message-tags"]; ok {
|
||||
for i := 0; i < c.state.enabledCap.Count(); i++ {
|
||||
if _, ok := c.state.enabledCap.Get("message-tags"); ok {
|
||||
in = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
if !in {
|
||||
event.Tags = Tags{}
|
||||
@ -517,13 +524,11 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
|
||||
c.debugLogEvent(event, false)
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastWrite = time.Now()
|
||||
c.conn.lastWrite.Store(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())
|
||||
@ -538,17 +543,14 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
|
||||
if event.Command == QUIT {
|
||||
c.Close()
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errs <- err
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -569,26 +571,25 @@ type ErrTimedOut struct {
|
||||
|
||||
func (ErrTimedOut) Error() string { return "timed out waiting for a requested PING response" }
|
||||
|
||||
func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||
func (c *Client) pingLoop(ctx context.Context, errs chan error, working *int32) {
|
||||
defer atomic.AddInt32(working, -1)
|
||||
// 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()
|
||||
c.conn.lastPing.Store(time.Now())
|
||||
c.conn.lastPong.Store(time.Now())
|
||||
|
||||
tick := time.NewTicker(c.Config.PingDelay)
|
||||
defer tick.Stop()
|
||||
|
||||
started := time.Now()
|
||||
past := false
|
||||
pingSent := false
|
||||
|
||||
for {
|
||||
select {
|
||||
@ -603,30 +604,27 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
||||
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,
|
||||
if pingSent && time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(180*time.Second) {
|
||||
// It's 180 seconds over what out ping delay is, connection has probably dropped.
|
||||
|
||||
err := ErrTimedOut{
|
||||
TimeSinceSuccess: time.Since(c.conn.lastPong.Load().(time.Time)),
|
||||
LastPong: c.conn.lastPong.Load().(time.Time),
|
||||
LastPing: c.conn.lastPing.Load().(time.Time),
|
||||
Delay: c.Config.PingDelay,
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
c.conn.mu.RUnlock()
|
||||
go func() {
|
||||
errs <- err
|
||||
}()
|
||||
return
|
||||
}
|
||||
c.conn.mu.RUnlock()
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPing = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
c.conn.lastPing.Store(time.Now())
|
||||
|
||||
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
pingSent = true
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
19
conn_test.go
19
conn_test.go
@ -8,16 +8,18 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func mockBuffers() (in *bytes.Buffer, out *bytes.Buffer, irc *ircConn) {
|
||||
func mockBuffers() (in, out *bytes.Buffer, irc *ircConn) {
|
||||
in = &bytes.Buffer{}
|
||||
out = &bytes.Buffer{}
|
||||
irc = &ircConn{
|
||||
io: bufio.NewReadWriter(bufio.NewReader(in), bufio.NewWriter(out)),
|
||||
connected: true,
|
||||
connected: atomic.Value{},
|
||||
}
|
||||
|
||||
return in, out, irc
|
||||
@ -46,8 +48,6 @@ func TestDecode(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatalf("should have failed to parse decoded event. got: %#v", event)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
@ -70,13 +70,11 @@ func TestEncode(t *testing.T) {
|
||||
if want != line {
|
||||
t.Fatalf("encoded line wanted: %q, got: %q", want, line)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestRate(t *testing.T) {
|
||||
_, _, c := mockBuffers()
|
||||
c.lastWrite = time.Now()
|
||||
c.lastWrite.Store(time.Now())
|
||||
if delay := c.rate(100); delay > time.Second {
|
||||
t.Fatal("first instance of rate is > second")
|
||||
}
|
||||
@ -88,17 +86,16 @@ func TestRate(t *testing.T) {
|
||||
if delay := c.rate(200); delay > (3 * time.Second) {
|
||||
t.Fatal("rate delay too high")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func genMockConn() (client *Client, clientConn net.Conn, serverConn net.Conn) {
|
||||
func genMockConn() (client *Client, clientConn, serverConn net.Conn) {
|
||||
client = New(Config{
|
||||
Server: "dummy.int",
|
||||
Port: 6667,
|
||||
Nick: "test",
|
||||
User: "test",
|
||||
Name: "Testing123",
|
||||
Debug: os.Stdout,
|
||||
})
|
||||
|
||||
conn1, conn2 := net.Pipe()
|
||||
@ -110,7 +107,7 @@ func mockReadBuffer(conn net.Conn) {
|
||||
// Accept all outgoing writes from the client.
|
||||
b := bufio.NewReader(conn)
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
_, err := b.ReadString(byte('\n'))
|
||||
if err != nil {
|
||||
return
|
||||
|
32
constants.go
32
constants.go
@ -5,6 +5,8 @@
|
||||
package girc
|
||||
|
||||
// Standard CTCP based constants.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
CTCP_ACTION = "ACTION"
|
||||
CTCP_PING = "PING"
|
||||
@ -20,6 +22,8 @@ const (
|
||||
|
||||
// Emulated event commands used to allow easier hooks into the changing
|
||||
// state of the client.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
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.
|
||||
@ -33,6 +37,8 @@ const (
|
||||
)
|
||||
|
||||
// User/channel prefixes :: RFC1459.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
DefaultPrefixes = "(ov)@+" // the most common default prefixes
|
||||
ModeAddPrefix = "+" // modes are being added
|
||||
@ -48,6 +54,8 @@ const (
|
||||
)
|
||||
|
||||
// User modes :: RFC1459; section 4.2.3.2.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
UserModeInvisible = "i" // invisible
|
||||
UserModeOperator = "o" // server operator
|
||||
@ -56,6 +64,8 @@ const (
|
||||
)
|
||||
|
||||
// Channel modes :: RFC1459; section 4.2.3.1.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
ModeDefaults = "beI,k,l,imnpst" // the most common default modes
|
||||
|
||||
@ -75,6 +85,8 @@ const (
|
||||
)
|
||||
|
||||
// IRC commands :: RFC2812; section 3 :: RFC2813; section 4.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
ADMIN = "ADMIN"
|
||||
AWAY = "AWAY"
|
||||
@ -127,6 +139,8 @@ const (
|
||||
)
|
||||
|
||||
// Numeric IRC reply mapping :: RFC2812; section 5.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
RPL_WELCOME = "001"
|
||||
RPL_YOURHOST = "002"
|
||||
@ -270,6 +284,8 @@ const (
|
||||
)
|
||||
|
||||
// IRCv3 commands and extensions :: http://ircv3.net/irc/.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
AUTHENTICATE = "AUTHENTICATE"
|
||||
MONITOR = "MONITOR"
|
||||
@ -293,6 +309,8 @@ const (
|
||||
)
|
||||
|
||||
// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
RPL_LOGGEDIN = "900"
|
||||
RPL_LOGGEDOUT = "901"
|
||||
@ -313,6 +331,8 @@ const (
|
||||
)
|
||||
|
||||
// Numeric IRC event mapping :: RFC2812; section 5.3.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
RPL_STATSCLINE = "213"
|
||||
RPL_STATSNLINE = "214"
|
||||
@ -341,10 +361,22 @@ const (
|
||||
)
|
||||
|
||||
// Misc.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
ERR_TOOMANYMATCHES = "416" // IRCNet.
|
||||
RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode.
|
||||
RPL_LOCALUSERS = "265" // aircd/hybrid/bahamut, used on freenode.
|
||||
RPL_TOPICWHOTIME = "333" // ircu, used on freenode.
|
||||
RPL_WHOSPCRPL = "354" // ircu, used on networks with WHOX support.
|
||||
RPL_CREATIONTIME = "329"
|
||||
)
|
||||
|
||||
// As seen in the wild.
|
||||
//
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
RPL_WHOISAUTHNAME = "330"
|
||||
RPL_WHOISTLS = "671"
|
||||
ERR_ALREADYOPER = "400"
|
||||
)
|
||||
|
95
ctcp.go
95
ctcp.go
@ -5,11 +5,12 @@
|
||||
package girc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
)
|
||||
|
||||
// ctcpDelim if the delimiter used for CTCP formatted events/messages.
|
||||
@ -104,8 +105,8 @@ func EncodeCTCP(ctcp *CTCPEvent) (out string) {
|
||||
// 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 ""
|
||||
if cmd == "" {
|
||||
return cmd
|
||||
}
|
||||
|
||||
out = string(ctcpDelim) + cmd
|
||||
@ -123,45 +124,31 @@ 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
|
||||
handlers cmap.ConcurrentMap[string, CTCPHandler]
|
||||
}
|
||||
|
||||
// newCTCP returns a new clean CTCP handler.
|
||||
func newCTCP() *CTCP {
|
||||
return &CTCP{handlers: map[string]CTCPHandler{}}
|
||||
return &CTCP{handlers: cmap.New[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 val, ok := c.handlers.Get("*"); ok && val != nil {
|
||||
val(client, *event)
|
||||
}
|
||||
|
||||
if _, ok := c.handlers[event.Command]; !ok {
|
||||
// If ACTION, don't do anything.
|
||||
if event.Command == CTCP_ACTION {
|
||||
return
|
||||
}
|
||||
|
||||
// Send a ERRMSG reply, if we know who sent it.
|
||||
if event.Source != nil && IsValidNick(event.Source.ID()) {
|
||||
client.Cmd.SendCTCPReply(event.Source.ID(), CTCP_ERRMSG, "that is an unknown CTCP query")
|
||||
}
|
||||
val, ok := c.handlers.Get(event.Command)
|
||||
if !ok || val == nil || event.Command == CTCP_ACTION {
|
||||
return
|
||||
}
|
||||
|
||||
c.handlers[event.Command](client, *event)
|
||||
val(client, *event)
|
||||
}
|
||||
|
||||
// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty
|
||||
@ -193,10 +180,7 @@ 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()
|
||||
c.handlers.Set(cmd, handler)
|
||||
}
|
||||
|
||||
// SetBg is much like Set, however the handler is executed in the background,
|
||||
@ -213,18 +197,12 @@ func (c *CTCP) Clear(cmd string) {
|
||||
if cmd = c.parseCMD(cmd); cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
delete(c.handlers, cmd)
|
||||
c.mu.Unlock()
|
||||
c.handlers.Remove(cmd)
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
c.handlers = cmap.New[CTCPHandler]()
|
||||
// Register necessary handlers.
|
||||
c.addDefaultHandlers()
|
||||
}
|
||||
@ -239,6 +217,8 @@ func (c *CTCP) addDefaultHandlers() {
|
||||
c.SetBg(CTCP_PONG, handleCTCPPong)
|
||||
c.SetBg(CTCP_VERSION, handleCTCPVersion)
|
||||
c.SetBg(CTCP_SOURCE, handleCTCPSource)
|
||||
c.SetBg(CTCP_USERINFO, handleCTCPUserInfo)
|
||||
c.SetBg(CTCP_CLIENTINFO, handleCTCPClientInfo)
|
||||
c.SetBg(CTCP_TIME, handleCTCPTime)
|
||||
c.SetBg(CTCP_FINGER, handleCTCPFinger)
|
||||
}
|
||||
@ -260,8 +240,7 @@ func handleCTCPPong(client *Client, ctcp CTCPEvent) {
|
||||
}
|
||||
|
||||
// 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).
|
||||
// as the os type if not overridden by client configuration.
|
||||
func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
|
||||
if client.Config.Version != "" {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_VERSION, client.Config.Version)
|
||||
@ -270,14 +249,34 @@ func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
|
||||
|
||||
client.Cmd.SendCTCPReplyf(
|
||||
ctcp.Source.ID(), CTCP_VERSION,
|
||||
"girc (github.com/lrstanley/girc) using %s (%s, %s)",
|
||||
runtime.Version(), runtime.GOOS, runtime.GOARCH,
|
||||
"girc-atomic %s (%s, %s)",
|
||||
Version, runtime.GOOS, runtime.GOARCH,
|
||||
)
|
||||
}
|
||||
|
||||
// handleCTCPSource replies with the public git location of this library.
|
||||
// handleCTCPUserInfo replies with the configured user information if available, otherwise it does not reply.
|
||||
func handleCTCPUserInfo(client *Client, ctcp CTCPEvent) {
|
||||
if client.Config.UserInfo == "" {
|
||||
return
|
||||
}
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_USERINFO, client.Config.UserInfo)
|
||||
}
|
||||
|
||||
// handleCTCPClientInfo replies with the configured client information if available, otherwise it does not reply.
|
||||
func handleCTCPClientInfo(client *Client, ctcp CTCPEvent) {
|
||||
if client.Config.UserInfo == "" {
|
||||
return
|
||||
}
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_USERINFO, client.Config.UserInfo)
|
||||
}
|
||||
|
||||
// handleCTCPUserInfo replies with the public git location of this library.
|
||||
func handleCTCPSource(client *Client, ctcp CTCPEvent) {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://github.com/lrstanley/girc")
|
||||
if client.Config.Source != "" {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, client.Config.Source)
|
||||
return
|
||||
}
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://github.com/yunginnanet/girc-atomic")
|
||||
}
|
||||
|
||||
// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's
|
||||
@ -289,9 +288,11 @@ func handleCTCPTime(client *Client, ctcp CTCPEvent) {
|
||||
// handleCTCPFinger replies with the realname and idle time of the user. This
|
||||
// is obsoleted by improvements to the IRC protocol, however still supported.
|
||||
func handleCTCPFinger(client *Client, ctcp CTCPEvent) {
|
||||
client.conn.mu.RLock()
|
||||
active := client.conn.lastActive
|
||||
client.conn.mu.RUnlock()
|
||||
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
||||
if client.Config.Finger != "" {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, client.Config.Finger)
|
||||
return
|
||||
}
|
||||
// irssi doesn't appear to do this on a stock install so gonna just go ahead and nix it.
|
||||
// active := client.conn.lastActive.Load().(time.Time)
|
||||
// client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
||||
}
|
||||
|
71
ctcp_test.go
71
ctcp_test.go
@ -9,26 +9,40 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var testsEncodeCTCP = []struct {
|
||||
name string
|
||||
test *CTCPEvent
|
||||
want string
|
||||
}{
|
||||
{name: "command only", test: &CTCPEvent{Command: "TEST", Text: ""}, want: "\001TEST\001"},
|
||||
{name: "command with args", test: &CTCPEvent{Command: "TEST", Text: "TEST"}, want: "\001TEST TEST\001"},
|
||||
{name: "nil command", test: &CTCPEvent{Command: "", Text: "TEST"}, want: ""},
|
||||
{name: "nil event", test: nil, want: ""},
|
||||
}
|
||||
|
||||
func FuzzEncodeCTCP(f *testing.F) {
|
||||
for _, tc := range testsEncodeCTCP {
|
||||
if tc.test == nil {
|
||||
continue
|
||||
}
|
||||
f.Add(tc.test.Command, tc.test.Text)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, cmd, text string) {
|
||||
got := EncodeCTCP(&CTCPEvent{Command: cmd, Text: text})
|
||||
|
||||
if utf8.ValidString(cmd) && utf8.ValidString(text) && !utf8.ValidString(got) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEncodeCTCP(t *testing.T) {
|
||||
type args struct {
|
||||
ctcp *CTCPEvent
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{name: "command only", args: args{ctcp: &CTCPEvent{Command: "TEST", Text: ""}}, want: "\001TEST\001"},
|
||||
{name: "command with args", args: args{ctcp: &CTCPEvent{Command: "TEST", Text: "TEST"}}, want: "\001TEST TEST\001"},
|
||||
{name: "nil command", args: args{ctcp: &CTCPEvent{Command: "", Text: "TEST"}}, want: ""},
|
||||
{name: "nil event", args: args{ctcp: nil}, want: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := EncodeCTCP(tt.args.ctcp); got != tt.want {
|
||||
for _, tt := range testsEncodeCTCP {
|
||||
if got := EncodeCTCP(tt.test); got != tt.want {
|
||||
t.Errorf("%s: encodeCTCP() = %q, want %q", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
@ -110,7 +124,8 @@ func TestCall(t *testing.T) {
|
||||
atomic.AddUint64(&counter, 1)
|
||||
})
|
||||
|
||||
if ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}); atomic.LoadUint64(&counter) != 1 {
|
||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
||||
if atomic.LoadUint64(&counter) != 1 {
|
||||
t.Fatal("regular execution: call() didn't increase counter")
|
||||
}
|
||||
ctcp.Clear("TEST")
|
||||
@ -120,7 +135,8 @@ func TestCall(t *testing.T) {
|
||||
})
|
||||
|
||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
||||
if time.Sleep(250 * time.Millisecond); atomic.LoadUint64(&counter) != 2 {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
if atomic.LoadUint64(&counter) != 2 {
|
||||
t.Fatal("goroutine execution: call() in goroutine didn't increase counter")
|
||||
}
|
||||
ctcp.Clear("TEST")
|
||||
@ -129,14 +145,15 @@ func TestCall(t *testing.T) {
|
||||
atomic.AddUint64(&counter, 1)
|
||||
})
|
||||
|
||||
if ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}); atomic.LoadUint64(&counter) != 3 {
|
||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
||||
if atomic.LoadUint64(&counter) != 3 {
|
||||
t.Fatal("wildcard execution: call() didn't increase counter")
|
||||
}
|
||||
ctcp.Clear("*")
|
||||
|
||||
ctcp.Clear("TEST")
|
||||
|
||||
if ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}); atomic.LoadUint64(&counter) != 3 {
|
||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
||||
if atomic.LoadUint64(&counter) != 3 {
|
||||
t.Fatal("empty execution: call() with no handler incremented the counter")
|
||||
}
|
||||
}
|
||||
@ -145,13 +162,13 @@ func TestSet(t *testing.T) {
|
||||
ctcp := newCTCP()
|
||||
|
||||
ctcp.Set("TEST-1", func(client *Client, event CTCPEvent) {})
|
||||
if _, ok := ctcp.handlers["TEST"]; ok {
|
||||
if _, ok := ctcp.handlers.Get("TEST"); ok {
|
||||
t.Fatal("Set('TEST') allowed invalid command")
|
||||
}
|
||||
|
||||
ctcp.Set("TEST", func(client *Client, event CTCPEvent) {})
|
||||
// Make sure it's there.
|
||||
if _, ok := ctcp.handlers["TEST"]; !ok {
|
||||
if _, ok := ctcp.handlers.Get("TEST"); !ok {
|
||||
t.Fatal("store: Set('TEST') didn't set")
|
||||
}
|
||||
}
|
||||
@ -162,7 +179,7 @@ func TestClear(t *testing.T) {
|
||||
ctcp.Set("TEST", func(client *Client, event CTCPEvent) {})
|
||||
ctcp.Clear("TEST")
|
||||
|
||||
if _, ok := ctcp.handlers["TEST"]; ok {
|
||||
if _, ok := ctcp.handlers.Get("TEST"); ok {
|
||||
t.Fatal("ctcp.Clear('TEST') didn't remove handler")
|
||||
}
|
||||
}
|
||||
@ -174,8 +191,8 @@ func TestClearAll(t *testing.T) {
|
||||
ctcp.Set("TEST2", func(client *Client, event CTCPEvent) {})
|
||||
ctcp.ClearAll()
|
||||
|
||||
_, first := ctcp.handlers["TEST1"]
|
||||
_, second := ctcp.handlers["TEST2"]
|
||||
_, first := ctcp.handlers.Get("TEST1")
|
||||
_, second := ctcp.handlers.Get("TEST2")
|
||||
|
||||
if first || second {
|
||||
t.Fatalf("ctcp.ClearAll() didn't remove all handlers: 1: %v 2: %v", first, second)
|
||||
|
52
event.go
52
event.go
@ -13,7 +13,7 @@ import (
|
||||
|
||||
const (
|
||||
eventSpace byte = ' ' // Separator.
|
||||
maxLength = 510 // Maximum length is 510 (2 for line endings).
|
||||
maxLength int = 510 // Maximum length is 510 (2 for line endings).
|
||||
)
|
||||
|
||||
// cutCRFunc is used to trim CR characters from prefixes/messages.
|
||||
@ -52,7 +52,7 @@ func ParseEvent(raw string) (e *Event) {
|
||||
i = 0
|
||||
}
|
||||
|
||||
if raw[0] == messagePrefix {
|
||||
if raw != "" && raw[0] == messagePrefix {
|
||||
// Prefix ends with a space.
|
||||
i = strings.IndexByte(raw, eventSpace)
|
||||
|
||||
@ -155,8 +155,10 @@ type Event struct {
|
||||
// Sensitive should be true if the message is sensitive (e.g. and should
|
||||
// not be logged/shown in debugging output).
|
||||
Sensitive bool `json:"sensitive"`
|
||||
// If the event is an echo-message response.
|
||||
// Echo is if the event is an echo-message response.
|
||||
Echo bool `json:"echo"`
|
||||
// Network represents the originating IRC network the event came from.
|
||||
Network string
|
||||
}
|
||||
|
||||
// Last returns the last parameter in Event.Params if it exists.
|
||||
@ -167,6 +169,16 @@ func (e *Event) Last() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsError does it's best to determine if the incoming event is tied to a known error event/command.
|
||||
// Note that if we are not aware of the given numeric IRC command, then this function will return false.
|
||||
// See: codebook.go
|
||||
func (e *Event) IsError() bool {
|
||||
if cmdstr, ok := IRCCodes[e.Command]; ok {
|
||||
return strings.Contains(cmdstr, "ERR_")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -248,7 +260,8 @@ func (e *Event) Len() (length int) {
|
||||
|
||||
// If param contains a space or it's empty, it's trailing, so it should be
|
||||
// prefixed with a colon (:).
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") {
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") ||
|
||||
strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
|
||||
length++
|
||||
}
|
||||
}
|
||||
@ -268,7 +281,9 @@ func (e *Event) Bytes() []byte {
|
||||
|
||||
// Tags.
|
||||
if e.Tags != nil {
|
||||
e.Tags.writeTo(buffer)
|
||||
if _, err := e.Tags.writeTo(buffer); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Event prefix.
|
||||
@ -284,9 +299,8 @@ func (e *Event) Bytes() []byte {
|
||||
// Space separated list of arguments.
|
||||
if len(e.Params) > 0 {
|
||||
// buffer.WriteByte(eventSpace)
|
||||
|
||||
for i := 0; i < len(e.Params); i++ {
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") {
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
|
||||
buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
|
||||
continue
|
||||
}
|
||||
@ -299,7 +313,9 @@ func (e *Event) Bytes() []byte {
|
||||
buffer.Truncate(maxLength)
|
||||
}
|
||||
|
||||
out := buffer.Bytes()
|
||||
// If we truncated in the middle of a utf8 character, we need to remove
|
||||
// the other (now invalid) bytes.
|
||||
out := bytes.ToValidUTF8(buffer.Bytes(), nil)
|
||||
|
||||
// Strip newlines and carriage returns.
|
||||
for i := 0; i < len(out); i++ {
|
||||
@ -363,7 +379,15 @@ func (e *Event) Pretty() (out string, ok bool) {
|
||||
|
||||
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[0:len(e.Params)-1], ","), e.Source.Name, e.Last()), true
|
||||
|
||||
var source string
|
||||
if e.Command == PRIVMSG {
|
||||
source = fmt.Sprintf("(%s)", e.Source.Name)
|
||||
} else { // NOTICE
|
||||
source = fmt.Sprintf("--%s--", e.Source.Name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s] %s %s", strings.Join(e.Params[0:len(e.Params)-1], ","), source, e.Last()), true
|
||||
}
|
||||
|
||||
if e.Command == RPL_MOTD || e.Command == RPL_MOTDSTART ||
|
||||
@ -596,14 +620,16 @@ func (s *Source) Bytes() []byte {
|
||||
|
||||
// String returns a string representation of source.
|
||||
func (s *Source) String() (out string) {
|
||||
out = s.Name
|
||||
out = ""
|
||||
if len(s.Name) > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -614,7 +640,7 @@ func (s *Source) IsHostmask() bool {
|
||||
|
||||
// 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
|
||||
return s.Ident == "" && s.Host == ""
|
||||
}
|
||||
|
||||
// writeTo is an utility function to write the source to the bytes.Buffer
|
||||
@ -629,6 +655,4 @@ func (s *Source) writeTo(buffer *bytes.Buffer) {
|
||||
buffer.WriteByte(prefixHost)
|
||||
buffer.WriteString(s.Host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -84,7 +84,10 @@ func TestParseEvent(t *testing.T) {
|
||||
{in: ":host.domain.com TEST\r\n", want: ":host.domain.com TEST"},
|
||||
{in: ":host.domain.com TEST arg1 arg2", want: ":host.domain.com TEST arg1 arg2"},
|
||||
{in: ":host.domain.com TEST :", want: ":host.domain.com TEST :"},
|
||||
{in: ":host.domain.com TEST ::", want: ":host.domain.com TEST ::"},
|
||||
{in: ":host.domain.com TEST :test1", want: ":host.domain.com TEST test1"},
|
||||
{in: ":host.domain.com TEST :test:test", want: ":host.domain.com TEST test:test"},
|
||||
{in: ":host.domain.com TEST :test1 :test", want: ":host.domain.com TEST :test1 :test"},
|
||||
{in: ":host.domain.com TEST :test1 test2", want: ":host.domain.com TEST :test1 test2"},
|
||||
{in: ":host.domain.com TEST arg1 arg2 :test1", want: ":host.domain.com TEST arg1 arg2 test1"},
|
||||
{in: ":host.domain.com TEST arg1 arg=:10 :test1", want: ":host.domain.com TEST arg1 arg=:10 test1"},
|
||||
@ -108,7 +111,7 @@ func TestParseEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
if got == nil {
|
||||
t.Errorf("ParseEvent: got nil, want: %s", tt.want)
|
||||
t.Fatalf("ParseEvent: got nil, want: %s", tt.want)
|
||||
}
|
||||
|
||||
if got.String() != tt.want {
|
||||
@ -130,6 +133,7 @@ func TestParseEvent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//goland:noinspection GoNilness
|
||||
func TestEventCopy(t *testing.T) {
|
||||
var nilEvent *Event
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lrstanley/girc"
|
||||
"github.com/yunginnanet/girc-atomic"
|
||||
)
|
||||
|
||||
func ExampleNew() {
|
||||
@ -35,7 +35,6 @@ func Example_bare() {
|
||||
Port: 6667,
|
||||
Nick: "test",
|
||||
User: "user",
|
||||
Debug: os.Stdout,
|
||||
})
|
||||
|
||||
if err := client.Connect(); err != nil {
|
||||
@ -52,7 +51,6 @@ func Example_simple() {
|
||||
Nick: "test",
|
||||
User: "user",
|
||||
Name: "Example bot",
|
||||
Debug: os.Stdout,
|
||||
})
|
||||
|
||||
client.Handlers.Add(girc.CONNECTED, func(c *girc.Client, e girc.Event) {
|
||||
@ -61,7 +59,7 @@ func Example_simple() {
|
||||
|
||||
client.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
|
||||
if strings.Contains(e.Last(), "hello") {
|
||||
c.Cmd.ReplyTo(e, "hello world!")
|
||||
_ = c.Cmd.ReplyTo(e, "hello world!")
|
||||
return
|
||||
}
|
||||
|
||||
@ -101,7 +99,7 @@ func Example_commands() {
|
||||
|
||||
client.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
|
||||
if strings.HasPrefix(e.Last(), "!hello") {
|
||||
c.Cmd.ReplyTo(e, girc.Fmt("{b}hello{b} {blue}world{c}!"))
|
||||
_ = c.Cmd.ReplyTo(e, girc.Fmt("{b}hello{b} {blue}world{c}!"))
|
||||
return
|
||||
}
|
||||
|
||||
|
41
format.go
41
format.go
@ -66,7 +66,7 @@ var fmtCodes = map[string]string{
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
|
||||
// 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++ {
|
||||
@ -127,10 +127,10 @@ func Fmt(text string) string {
|
||||
// See Fmt() for more information.
|
||||
func TrimFmt(text string) string {
|
||||
for color := range fmtColors {
|
||||
text = strings.Replace(text, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
|
||||
text = strings.ReplaceAll(text, string(fmtOpenChar)+color+string(fmtCloseChar), "")
|
||||
}
|
||||
for code := range fmtCodes {
|
||||
text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
|
||||
text = strings.ReplaceAll(text, string(fmtOpenChar)+code+string(fmtCloseChar), "")
|
||||
}
|
||||
|
||||
return text
|
||||
@ -138,7 +138,7 @@ func TrimFmt(text string) string {
|
||||
|
||||
// This is really the only fastest way of doing this (marginally better than
|
||||
// actually trying to parse it manually.)
|
||||
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)
|
||||
var reStripColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)?`)
|
||||
|
||||
// StripRaw tries to strip all ASCII format codes that are used for IRC.
|
||||
// Primarily, foreground/background colors, and other control bytes like
|
||||
@ -148,7 +148,7 @@ func StripRaw(text string) string {
|
||||
text = reStripColor.ReplaceAllString(text, "")
|
||||
|
||||
for _, code := range fmtCodes {
|
||||
text = strings.Replace(text, code, "", -1)
|
||||
text = strings.ReplaceAll(text, code, "")
|
||||
}
|
||||
|
||||
return text
|
||||
@ -164,12 +164,12 @@ func StripRaw(text string) string {
|
||||
// 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 )
|
||||
// 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
|
||||
@ -214,12 +214,12 @@ func IsValidChannel(channel string) bool {
|
||||
// IsValidNick validates an IRC nickname. Note that this does not validate
|
||||
// IRC nickname length.
|
||||
//
|
||||
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
|
||||
// letter = 0x41-0x5A / 0x61-0x7A
|
||||
// digit = 0x30-0x39
|
||||
// special = 0x5B-0x60 / 0x7B-0x7D
|
||||
// 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 {
|
||||
if nick == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -253,10 +253,11 @@ func IsValidNick(nick string) bool {
|
||||
// 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 "@"
|
||||
//
|
||||
// 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 {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -324,7 +325,7 @@ func Glob(input, match string) bool {
|
||||
|
||||
if len(parts) == 1 {
|
||||
// No globs, test for equality.
|
||||
return input == match
|
||||
return strings.EqualFold(input, match)
|
||||
}
|
||||
|
||||
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
|
||||
|
466
format_test.go
466
format_test.go
@ -7,38 +7,31 @@ package girc
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func BenchmarkFormat(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Fmt("{red}test{c}")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkFormatLong(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Fmt("{red}test {blue}2 {red}3 {brown} {italic}test{c}")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkStripFormat(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
TrimFmt("{red}test{c}")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkStripFormatLong(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
TrimFmt("{red}test {blue}2 {red}3 {brown} {italic}test{c}")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkStripRaw(b *testing.B) {
|
||||
@ -46,8 +39,6 @@ func BenchmarkStripRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
StripRaw(text)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkStripRawLong(b *testing.B) {
|
||||
@ -55,202 +46,287 @@ func BenchmarkStripRawLong(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
StripRaw(text)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
var testsFormat = []struct {
|
||||
name string
|
||||
test string
|
||||
want string
|
||||
}{
|
||||
{name: "middle", test: "test{red}test{c}test", want: "test\x0304test\x03test"},
|
||||
{name: "middle with bold", test: "test{red}{b}test{c}test", want: "test\x0304\x02test\x03test"},
|
||||
{name: "start, end", test: "{red}test{c}", want: "\x0304test\x03"},
|
||||
{name: "start, middle, end", test: "{red}te{red}st{c}", want: "\x0304te\x0304st\x03"},
|
||||
{name: "partial", test: "{redtest{c}", want: "{redtest\x03"},
|
||||
{name: "inside", test: "{re{c}d}test{c}", want: "{re\x03d}test\x03"},
|
||||
{name: "nothing", test: "this is a test.", want: "this is a test."},
|
||||
{name: "fg and bg", test: "{red,yellow}test{c}", want: "\x0304,08test\x03"},
|
||||
{name: "just bg", test: "{,yellow}test{c}", want: "test\x03"},
|
||||
{name: "just red", test: "{red}test", want: "\x0304test"},
|
||||
{name: "just cyan", test: "{cyan}test", want: "\x0311test"},
|
||||
}
|
||||
|
||||
func FuzzFormat(f *testing.F) {
|
||||
for _, tc := range testsFormat {
|
||||
f.Add(tc.test)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
got := Fmt(orig)
|
||||
got2 := Fmt(got)
|
||||
|
||||
if utf8.ValidString(orig) {
|
||||
if !utf8.ValidString(got) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
||||
}
|
||||
|
||||
if !utf8.ValidString(got2) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got2)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
type args struct {
|
||||
text string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{name: "middle", args: args{text: "test{red}test{c}test"}, want: "test\x0304test\x03test"},
|
||||
{name: "middle with bold", args: args{text: "test{red}{b}test{c}test"}, want: "test\x0304\x02test\x03test"},
|
||||
{name: "start, end", args: args{text: "{red}test{c}"}, want: "\x0304test\x03"},
|
||||
{name: "start, middle, end", args: args{text: "{red}te{red}st{c}"}, want: "\x0304te\x0304st\x03"},
|
||||
{name: "partial", args: args{text: "{redtest{c}"}, want: "{redtest\x03"},
|
||||
{name: "inside", args: args{text: "{re{c}d}test{c}"}, want: "{re\x03d}test\x03"},
|
||||
{name: "nothing", args: args{text: "this is a test."}, want: "this is a test."},
|
||||
{name: "fg and bg", args: args{text: "{red,yellow}test{c}"}, want: "\x0304,08test\x03"},
|
||||
{name: "just bg", args: args{text: "{,yellow}test{c}"}, want: "test\x03"},
|
||||
{name: "just red", args: args{text: "{red}test"}, want: "\x0304test"},
|
||||
{name: "just cyan", args: args{text: "{cyan}test"}, want: "\x0311test"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := Fmt(tt.args.text); got != tt.want {
|
||||
t.Errorf("%s: Format(%q) = %q, want %q", tt.name, tt.args.text, got, tt.want)
|
||||
for _, tt := range testsFormat {
|
||||
if got := Fmt(tt.test); got != tt.want {
|
||||
t.Errorf("%s: Format(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testsStripFormat = []struct {
|
||||
name string
|
||||
test string
|
||||
want string
|
||||
}{
|
||||
{name: "start, end", test: "{red}test{c}", want: "test"},
|
||||
{name: "start, middle, end", test: "{red}te{red}st{c}", want: "test"},
|
||||
{name: "partial", test: "{redtest{c}", want: "{redtest"},
|
||||
{name: "inside", test: "{re{c}d}test{c}", want: "{red}test"},
|
||||
{name: "nothing", test: "this is a test.", want: "this is a test."},
|
||||
}
|
||||
|
||||
func FuzzStripFormat(f *testing.F) {
|
||||
for _, tc := range testsStripFormat {
|
||||
f.Add(tc.test)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
got := TrimFmt(orig)
|
||||
got2 := TrimFmt(got)
|
||||
|
||||
if utf8.ValidString(orig) {
|
||||
if !utf8.ValidString(got) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
||||
}
|
||||
|
||||
if !utf8.ValidString(got2) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got2)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStripFormat(t *testing.T) {
|
||||
type args struct {
|
||||
text string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{name: "start, end", args: args{text: "{red}test{c}"}, want: "test"},
|
||||
{name: "start, middle, end", args: args{text: "{red}te{red}st{c}"}, want: "test"},
|
||||
{name: "partial", args: args{text: "{redtest{c}"}, want: "{redtest"},
|
||||
{name: "inside", args: args{text: "{re{c}d}test{c}"}, want: "{red}test"},
|
||||
{name: "nothing", args: args{text: "this is a test."}, want: "this is a test."},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := TrimFmt(tt.args.text); got != tt.want {
|
||||
t.Errorf("%s: StripFormat(%q) = %q, want %q", tt.name, tt.args.text, got, tt.want)
|
||||
for _, tt := range testsStripFormat {
|
||||
if got := TrimFmt(tt.test); got != tt.want {
|
||||
t.Errorf("%s: StripFormat(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testsStripRaw = []struct {
|
||||
name string
|
||||
test string // gets passed to Format() before sent
|
||||
want string
|
||||
}{
|
||||
{name: "start, end", test: "{red}{b}test{c}", want: "test"},
|
||||
{name: "start, end in numbers", test: "{red}1234{c}", want: "1234"},
|
||||
{name: "start, middle, end", test: "{red}te{red}st{c}", want: "test"},
|
||||
{name: "partial", test: "{redtest{c}", want: "{redtest"},
|
||||
{name: "inside", test: "{re{c}d}test{c}", want: "{red}test"},
|
||||
{name: "fg+bg colors start", test: "{red,yellow}test{c}", want: "test"},
|
||||
{name: "fg+bg colors start in numbers", test: "{red,yellow}1234{c}", want: "1234"},
|
||||
{name: "fg+bg colors end", test: "test{,yellow}", want: "test"},
|
||||
{name: "bg colors start", test: "{,yellow}test{c}", want: "test"},
|
||||
{name: "inside", test: "{re{c}d}test{c}", want: "{red}test"},
|
||||
{name: "nothing", test: "this is a test.", want: "this is a test."},
|
||||
}
|
||||
|
||||
func FuzzStripRaw(f *testing.F) {
|
||||
for _, tc := range testsStripRaw {
|
||||
f.Add(tc.test)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
got := StripRaw(orig)
|
||||
got2 := StripRaw(got)
|
||||
|
||||
if utf8.ValidString(orig) {
|
||||
if !utf8.ValidString(got) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
||||
}
|
||||
|
||||
if !utf8.ValidString(got2) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got2)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStripRaw(t *testing.T) {
|
||||
type args struct {
|
||||
text string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args // gets passed to Format() before sent
|
||||
want string
|
||||
}{
|
||||
{name: "start, end", args: args{text: "{red}{b}test{c}"}, want: "test"},
|
||||
{name: "start, end in numbers", args: args{text: "{red}1234{c}"}, want: "1234"},
|
||||
{name: "start, middle, end", args: args{text: "{red}te{red}st{c}"}, want: "test"},
|
||||
{name: "partial", args: args{text: "{redtest{c}"}, want: "{redtest"},
|
||||
{name: "inside", args: args{text: "{re{c}d}test{c}"}, want: "{red}test"},
|
||||
{name: "fg+bg colors start", args: args{text: "{red,yellow}test{c}"}, want: "test"},
|
||||
{name: "fg+bg colors start in numbers", args: args{text: "{red,yellow}1234{c}"}, want: "1234"},
|
||||
{name: "fg+bg colors end", args: args{text: "test{,yellow}"}, want: "test"},
|
||||
{name: "bg colors start", args: args{text: "{,yellow}test{c}"}, want: "test"},
|
||||
{name: "inside", args: args{text: "{re{c}d}test{c}"}, want: "{red}test"},
|
||||
{name: "nothing", args: args{text: "this is a test."}, want: "this is a test."},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := StripRaw(Fmt(tt.args.text)); got != tt.want {
|
||||
t.Fatalf("%s: StripRaw(%q) = %q, want %q", tt.name, tt.args.text, got, tt.want)
|
||||
for _, tt := range testsStripRaw {
|
||||
if got := StripRaw(Fmt(tt.test)); got != tt.want {
|
||||
t.Fatalf("%s: StripRaw(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testsValidNick = []struct {
|
||||
name string
|
||||
test string
|
||||
want bool
|
||||
}{
|
||||
{name: "normal", test: "test", want: true},
|
||||
{name: "empty", test: "", want: false},
|
||||
{name: "hyphen and special", test: "test[-]", want: true},
|
||||
{name: "invalid middle", test: "test!test", want: false},
|
||||
{name: "invalid dot middle", test: "test.test", want: false},
|
||||
{name: "end", test: "test!", want: false},
|
||||
{name: "invalid start", test: "!test", want: false},
|
||||
{name: "backslash and numeric", test: "test[\\0", want: true},
|
||||
{name: "long", test: "test123456789AZBKASDLASMDLKM", want: true},
|
||||
{name: "index 0 dash", test: "-test", want: false},
|
||||
{name: "index 0 numeric", test: "0test", want: false},
|
||||
{name: "RFC1459 non-lowercase-converted", test: "test^", want: true},
|
||||
{name: "RFC1459 non-lowercase-converted", test: "test~", want: false},
|
||||
}
|
||||
|
||||
func FuzzValidNick(f *testing.F) {
|
||||
for _, tc := range testsValidNick {
|
||||
f.Add(tc.test)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
_ = IsValidNick(orig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsValidNick(t *testing.T) {
|
||||
type args struct {
|
||||
nick string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{name: "normal", args: args{nick: "test"}, want: true},
|
||||
{name: "empty", args: args{nick: ""}, want: false},
|
||||
{name: "hyphen and special", args: args{nick: "test[-]"}, want: true},
|
||||
{name: "invalid middle", args: args{nick: "test!test"}, want: false},
|
||||
{name: "invalid dot middle", args: args{nick: "test.test"}, want: false},
|
||||
{name: "end", args: args{nick: "test!"}, want: false},
|
||||
{name: "invalid start", args: args{nick: "!test"}, want: false},
|
||||
{name: "backslash and numeric", args: args{nick: "test[\\0"}, want: true},
|
||||
{name: "long", args: args{nick: "test123456789AZBKASDLASMDLKM"}, want: true},
|
||||
{name: "index 0 dash", args: args{nick: "-test"}, want: false},
|
||||
{name: "index 0 numeric", args: args{nick: "0test"}, want: false},
|
||||
{name: "RFC1459 non-lowercase-converted", args: args{nick: "test^"}, want: true},
|
||||
{name: "RFC1459 non-lowercase-converted", args: args{nick: "test~"}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := IsValidNick(tt.args.nick); got != tt.want {
|
||||
t.Errorf("%s: IsValidNick(%q) = %v, want %v", tt.name, tt.args.nick, got, tt.want)
|
||||
for _, tt := range testsValidNick {
|
||||
if got := IsValidNick(tt.test); got != tt.want {
|
||||
t.Errorf("%s: IsValidNick(%q) = %v, want %v", tt.name, tt.test, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testsValidChannel = []struct {
|
||||
name string
|
||||
test string
|
||||
want bool
|
||||
}{
|
||||
{name: "valid channel", test: "#valid", want: true},
|
||||
{name: "invalid channel comma", test: "#invalid,", want: false},
|
||||
{name: "invalid channel space", test: "#inva lid", want: false},
|
||||
{name: "valid channel with numerics", test: "#1valid0", want: true},
|
||||
{name: "valid channel with special", test: "#valid[]test", want: true},
|
||||
{name: "valid channel with special", test: "#[]valid[]test[]", want: true},
|
||||
{name: "just hash", test: "#", want: false},
|
||||
{name: "empty", test: "", want: false},
|
||||
{name: "invalid prefix", test: "$invalid", want: false},
|
||||
{name: "too long", test: "#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", want: false},
|
||||
{name: "valid id prefix", test: "!12345test", want: true},
|
||||
{name: "invalid id length", test: "!1234", want: false},
|
||||
{name: "invalid id length", test: "!12345", want: false},
|
||||
{name: "invalid id prefix", test: "!test1invalid", want: false},
|
||||
}
|
||||
|
||||
func FuzzValidChannel(f *testing.F) {
|
||||
for _, tc := range testsValidChannel {
|
||||
f.Add(tc.test)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
_ = IsValidChannel(orig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsValidChannel(t *testing.T) {
|
||||
type args struct {
|
||||
channel string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{name: "valid channel", args: args{channel: "#valid"}, want: true},
|
||||
{name: "invalid channel comma", args: args{channel: "#invalid,"}, want: false},
|
||||
{name: "invalid channel space", args: args{channel: "#inva lid"}, want: false},
|
||||
{name: "valid channel with numerics", args: args{channel: "#1valid0"}, want: true},
|
||||
{name: "valid channel with special", args: args{channel: "#valid[]test"}, want: true},
|
||||
{name: "valid channel with special", args: args{channel: "#[]valid[]test[]"}, want: true},
|
||||
{name: "just hash", args: args{channel: "#"}, want: false},
|
||||
{name: "empty", args: args{channel: ""}, want: false},
|
||||
{name: "invalid prefix", args: args{channel: "$invalid"}, want: false},
|
||||
{name: "too long", args: args{channel: "#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, want: false},
|
||||
{name: "valid id prefix", args: args{channel: "!12345test"}, want: true},
|
||||
{name: "invalid id length", args: args{channel: "!1234"}, want: false},
|
||||
{name: "invalid id length", args: args{channel: "!12345"}, want: false},
|
||||
{name: "invalid id prefix", args: args{channel: "!test1invalid"}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := IsValidChannel(tt.args.channel); got != tt.want {
|
||||
t.Errorf("%s: IsValidChannel(%q) = %v, want %v", tt.name, tt.args.channel, got, tt.want)
|
||||
for _, tt := range testsValidChannel {
|
||||
if got := IsValidChannel(tt.test); got != tt.want {
|
||||
t.Errorf("%s: IsValidChannel(%q) = %v, want %v", tt.name, tt.test, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testsValidUser = []struct {
|
||||
name string
|
||||
test string
|
||||
want bool
|
||||
}{
|
||||
{name: "user without ident server", test: "~test", want: true},
|
||||
{name: "user with ident server", test: "test", want: true},
|
||||
{name: "non-alphanumeric first index", test: "-test", want: false},
|
||||
{name: "non-alphanumeric first index", test: "[test]", want: false},
|
||||
{name: "numeric first index", test: "0test", want: true},
|
||||
{name: "blank", test: "", want: false},
|
||||
{name: "just tilde", test: "~", want: false},
|
||||
{name: "special chars", test: "test-----", want: true},
|
||||
{name: "special chars", test: "test-[]-", want: true},
|
||||
{name: "special chars, invalid after first index", test: "t!--", want: false},
|
||||
}
|
||||
|
||||
func FuzzValidUser(f *testing.F) {
|
||||
for _, tc := range testsValidUser {
|
||||
f.Add(tc.test)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
_ = IsValidUser(orig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsValidUser(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{name: "user without ident server", args: args{name: "~test"}, want: true},
|
||||
{name: "user with ident server", args: args{name: "test"}, want: true},
|
||||
{name: "non-alphanumeric first index", args: args{name: "-test"}, want: false},
|
||||
{name: "non-alphanumeric first index", args: args{name: "[test]"}, want: false},
|
||||
{name: "numeric first index", args: args{name: "0test"}, want: true},
|
||||
{name: "blank", args: args{name: ""}, want: false},
|
||||
{name: "just tilde", args: args{name: "~"}, want: false},
|
||||
{name: "special chars", args: args{name: "test-----"}, want: true},
|
||||
{name: "special chars", args: args{name: "test-[]-"}, want: true},
|
||||
{name: "special chars, invalid after first index", args: args{name: "t!--"}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := IsValidUser(tt.args.name); got != tt.want {
|
||||
t.Errorf("%s: IsValidUser(%q) = %v, want %v", tt.name, tt.args.name, got, tt.want)
|
||||
for _, tt := range testsValidUser {
|
||||
if got := IsValidUser(tt.test); got != tt.want {
|
||||
t.Errorf("%s: IsValidUser(%q) = %v, want %v", tt.name, tt.test, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToRFC1459(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abcd", "abcd"},
|
||||
{"AbcD", "abcd"},
|
||||
{"!@#$%^&*()_+-=", "!@#$%~&*()_+-="},
|
||||
{"Abcd[]", "abcd{}"},
|
||||
var testsToRFC1459 = []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abcd", "abcd"},
|
||||
{"AbcD", "abcd"},
|
||||
{"!@#$%^&*()_+-=", "!@#$%~&*()_+-="},
|
||||
{"Abcd[]", "abcd{}"},
|
||||
}
|
||||
|
||||
func FuzzToRFC1459(f *testing.F) {
|
||||
for _, tc := range testsToRFC1459 {
|
||||
f.Add(tc.in)
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
got := ToRFC1459(orig)
|
||||
|
||||
if utf8.ValidString(orig) && !utf8.ValidString(got) {
|
||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestToRFC1459(t *testing.T) {
|
||||
for _, tt := range testsToRFC1459 {
|
||||
if got := ToRFC1459(tt.in); got != tt.want {
|
||||
t.Errorf("ToRFC1459() = %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BenchmarkGlob(b *testing.B) {
|
||||
@ -259,31 +335,23 @@ func BenchmarkGlob(b *testing.B) {
|
||||
b.Fatalf("should match")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func testGlobMatch(t *testing.T, subj, pattern string) {
|
||||
if !Glob(subj, pattern) {
|
||||
t.Fatalf("'%s' should match '%s'", pattern, subj)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func testGlobNoMatch(t *testing.T, subj, pattern string) {
|
||||
if Glob(subj, pattern) {
|
||||
t.Fatalf("'%s' should not match '%s'", pattern, subj)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestEmptyPattern(t *testing.T) {
|
||||
testGlobMatch(t, "", "")
|
||||
testGlobNoMatch(t, "test", "")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestEmptySubject(t *testing.T) {
|
||||
@ -324,37 +392,43 @@ func TestEmptySubject(t *testing.T) {
|
||||
for _, pattern := range cases {
|
||||
testGlobNoMatch(t, pattern, "")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestPatternWithoutGlobs(t *testing.T) {
|
||||
testGlobMatch(t, "test", "test")
|
||||
}
|
||||
|
||||
return
|
||||
var testsGlob = []string{
|
||||
"*test", // Leading.
|
||||
"this*", // Trailing.
|
||||
"this*test", // Middle.
|
||||
"*is *", // String in between two.
|
||||
"*is*a*", // Lots.
|
||||
"**test**", // Double glob characters.
|
||||
"**is**a***test*", // Varying number.
|
||||
"* *", // White space between.
|
||||
"*", // Lone.
|
||||
"**********", // Nothing but globs.
|
||||
"*Ѿ*", // Unicode.
|
||||
"*is a ϗѾ *", // Mixed ASCII/unicode.
|
||||
}
|
||||
|
||||
func FuzzGlob(f *testing.F) {
|
||||
for _, tc := range testsGlob {
|
||||
f.Add(tc, tc)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, orig, orig2 string) {
|
||||
_ = Glob(orig, orig2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGlob(t *testing.T) {
|
||||
cases := []string{
|
||||
"*test", // Leading.
|
||||
"this*", // Trailing.
|
||||
"this*test", // Middle.
|
||||
"*is *", // String in between two.
|
||||
"*is*a*", // Lots.
|
||||
"**test**", // Double glob characters.
|
||||
"**is**a***test*", // Varying number.
|
||||
"* *", // White space between.
|
||||
"*", // Lone.
|
||||
"**********", // Nothing but globs.
|
||||
"*Ѿ*", // Unicode.
|
||||
"*is a ϗѾ *", // Mixed ASCII/unicode.
|
||||
}
|
||||
|
||||
for _, pattern := range cases {
|
||||
for _, pattern := range testsGlob {
|
||||
testGlobMatch(t, "this is a ϗѾ test", pattern)
|
||||
}
|
||||
|
||||
cases = []string{
|
||||
cases := []string{
|
||||
"test*", // Implicit substring match.
|
||||
"*is", // Partial match.
|
||||
"*no*", // Globs without a match between them.
|
||||
@ -368,6 +442,4 @@ func TestGlob(t *testing.T) {
|
||||
for _, pattern := range cases {
|
||||
testGlobNoMatch(t, "this is a test", pattern)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
10
go.mod
10
go.mod
@ -1,3 +1,9 @@
|
||||
module github.com/lrstanley/girc
|
||||
module github.com/yunginnanet/girc-atomic
|
||||
|
||||
go 1.12
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
git.tcp.direct/kayos/common v0.8.1
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||
)
|
||||
|
19
go.sum
19
go.sum
@ -0,0 +1,19 @@
|
||||
git.tcp.direct/kayos/common v0.8.1 h1:gxcCaa7QlQzkvBPzcwoVyP89mexrxKvnmlnvh4PGu4o=
|
||||
git.tcp.direct/kayos/common v0.8.1/go.mod h1:r7lZuKTQz0uf/jNm61sz1XaMgK/RYRr7wtqr/cNYd8o=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
257
handler.go
257
handler.go
@ -12,24 +12,31 @@ import (
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
)
|
||||
|
||||
// RunHandlers manually runs handlers for a given event.
|
||||
func (c *Client) RunHandlers(event *Event) {
|
||||
if event == nil {
|
||||
c.debug.Print("nil event")
|
||||
return
|
||||
}
|
||||
|
||||
s := strs.Get()
|
||||
// Log the event.
|
||||
prefix := "< "
|
||||
s.MustWriteString("< ")
|
||||
if event.Echo {
|
||||
prefix += "[echo-message] "
|
||||
s.MustWriteString("[echo-message] ")
|
||||
}
|
||||
c.debug.Print(prefix + StripRaw(event.String()))
|
||||
s.MustWriteString(event.String())
|
||||
c.debug.Print(s.String())
|
||||
strs.MustPut(s)
|
||||
if c.Config.Out != nil {
|
||||
if pretty, ok := event.Pretty(); ok {
|
||||
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||
_, _ = fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,15 +48,16 @@ func (c *Client) RunHandlers(event *Event) {
|
||||
}
|
||||
|
||||
c.Handlers.exec(ALL_EVENTS, false, c, event.Copy())
|
||||
|
||||
if !event.Echo {
|
||||
c.Handlers.exec(event.Command, false, c, event.Copy())
|
||||
}
|
||||
|
||||
// Check if it's a CTCP.
|
||||
if ctcp := DecodeCTCP(event.Copy()); ctcp != nil {
|
||||
// Execute it.
|
||||
c.CTCP.call(c, ctcp)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Handler is lower level implementation of a handler. See
|
||||
@ -67,29 +75,87 @@ func (f HandlerFunc) Execute(client *Client, event Event) {
|
||||
f(client, event)
|
||||
}
|
||||
|
||||
// nestedHandlers consists of a nested concurrent map.
|
||||
//
|
||||
// ( cmap.ConcurrentMap[command]cmap.ConcurrentMap[cuid]Handler )
|
||||
//
|
||||
// command and cuid are both strings.
|
||||
type nestedHandlers struct {
|
||||
cm cmap.ConcurrentMap[string, cmap.ConcurrentMap[string, Handler]]
|
||||
}
|
||||
|
||||
type handlerTuple struct {
|
||||
cuid string
|
||||
handler Handler
|
||||
}
|
||||
|
||||
func newNestedHandlers() *nestedHandlers {
|
||||
return &nestedHandlers{cm: cmap.New[cmap.ConcurrentMap[string, Handler]]()}
|
||||
}
|
||||
|
||||
func (nest *nestedHandlers) len() (total int) {
|
||||
for hndlrs := range nest.cm.IterBuffered() {
|
||||
total += len(hndlrs.Val.Keys())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (nest *nestedHandlers) lenFor(cmd string) (total int) {
|
||||
cmd = strings.ToUpper(cmd)
|
||||
hndlrs, ok := nest.cm.Get(cmd)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return hndlrs.Count()
|
||||
}
|
||||
|
||||
func (nest *nestedHandlers) getAllHandlersFor(s string) (handlers chan handlerTuple, ok bool) {
|
||||
var h cmap.ConcurrentMap[string, Handler]
|
||||
h, ok = nest.cm.Get(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
handlers = make(chan handlerTuple)
|
||||
go func() {
|
||||
for hi := range h.IterBuffered() {
|
||||
ht := handlerTuple{
|
||||
hi.Key,
|
||||
hi.Val,
|
||||
}
|
||||
handlers <- ht
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
mu *sync.RWMutex
|
||||
|
||||
parent *Client
|
||||
|
||||
// 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
|
||||
external *nestedHandlers
|
||||
// external map[string]map[string]Handler
|
||||
// internal is a map of internally used handlers for the client.
|
||||
internal map[string]map[string]Handler
|
||||
internal *nestedHandlers
|
||||
// debug is the clients logger used for debugging.
|
||||
debug *log.Logger
|
||||
}
|
||||
|
||||
// newCaller creates and initializes a new handler.
|
||||
func newCaller(debugOut *log.Logger) *Caller {
|
||||
func newCaller(parent *Client, debugOut *log.Logger) *Caller {
|
||||
c := &Caller{
|
||||
external: map[string]map[string]Handler{},
|
||||
internal: map[string]map[string]Handler{},
|
||||
external: newNestedHandlers(),
|
||||
internal: newNestedHandlers(),
|
||||
debug: debugOut,
|
||||
parent: parent,
|
||||
mu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
return c
|
||||
@ -97,45 +163,18 @@ func newCaller(debugOut *log.Logger) *Caller {
|
||||
|
||||
// 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
|
||||
return c.external.len()
|
||||
}
|
||||
|
||||
// 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
|
||||
return c.external.lenFor(cmd)
|
||||
}
|
||||
|
||||
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)
|
||||
return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), c.internal.len())
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
@ -173,88 +212,90 @@ type execStack struct {
|
||||
// Please note that there is no specific order/priority for which the handlers
|
||||
// are executed.
|
||||
func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
// 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] {
|
||||
hmap, iok := c.internal.cm.Get(command)
|
||||
if iok {
|
||||
for assigned := range hmap.IterBuffered() {
|
||||
cuid := assigned.Key
|
||||
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
|
||||
continue
|
||||
}
|
||||
|
||||
stack = append(stack, execStack{c.internal[command][cuid], cuid})
|
||||
hndlr, _ := hmap.Get(cuid)
|
||||
stack = append(stack, execStack{hndlr, cuid})
|
||||
}
|
||||
}
|
||||
|
||||
// Then external handlers.
|
||||
if _, ok := c.external[command]; ok {
|
||||
for cuid := range c.external[command] {
|
||||
hmap, eok := c.external.cm.Get(command)
|
||||
if eok {
|
||||
for _, cuid := range hmap.Keys() {
|
||||
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
|
||||
continue
|
||||
}
|
||||
|
||||
stack = append(stack, execStack{c.external[command][cuid], cuid})
|
||||
hndlr, _ := hmap.Get(cuid)
|
||||
stack = append(stack, execStack{hndlr, 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))
|
||||
var working int32
|
||||
atomic.AddInt32(&working, int32(len(stack)))
|
||||
// c.debug.Printf("starting %d jobs", atomic.LoadInt32(&working))
|
||||
for i := 0; i < len(stack); i++ {
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
c.debug.Printf("[%d/%d] exec %s => %s", index+1, len(stack), stack[index].cuid, command)
|
||||
start := time.Now()
|
||||
// c.debug.Printf("(%s) [%d/%d] exec %s => %s", c.parent.Config.Nick,
|
||||
// index+1, len(stack), stack[index].cuid, command)
|
||||
// start := time.Now()
|
||||
|
||||
if bg {
|
||||
go func() {
|
||||
defer atomic.AddInt32(&working, -1)
|
||||
if client.Config.RecoverFunc != nil {
|
||||
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
||||
}
|
||||
|
||||
stack[index].Execute(client, *event)
|
||||
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
|
||||
stack[index].Handler.Execute(client, *event)
|
||||
// c.debug.Printf("(%s) done %s == %s", c.parent.Config.Nick,
|
||||
// stack[index].cuid, time.Since(start))
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
defer atomic.AddInt32(&working, -1)
|
||||
|
||||
if client.Config.RecoverFunc != nil {
|
||||
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
||||
}
|
||||
|
||||
stack[index].Execute(client, *event)
|
||||
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
|
||||
stack[index].Handler.Execute(client, *event)
|
||||
// c.debug.Printf("(%s) done %s == %s", c.parent.Config.Nick, stack[index].cuid, time.Since(start))
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all of the handlers to complete. Not doing this may cause
|
||||
// new events from becoming ahead of older handlers.
|
||||
wg.Wait()
|
||||
// new events from becoming ahead of ol1 handlers.
|
||||
// c.debug.Printf("(%s) atomic.CompareAndSwap: %d jobs running", c.parent.Config.Nick, atomic.LoadInt32(&working))
|
||||
|
||||
if atomic.CompareAndSwapInt32(&working, 0, -1) {
|
||||
// c.debug.Printf("(%s) exec stack completed", c.parent.Config.Nick)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.external.cm.Clear()
|
||||
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.internal.cm.Clear()
|
||||
c.debug.Print("cleared all internal handlers")
|
||||
}
|
||||
|
||||
@ -262,46 +303,39 @@ func (c *Caller) clearInternal() {
|
||||
// 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)
|
||||
c.external.cm.Remove(cmd)
|
||||
c.debug.Printf("(%s) cleared external handlers for %s", c.parent.Config.Nick, 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
|
||||
c.remove(cuid)
|
||||
return true
|
||||
}
|
||||
|
||||
// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu
|
||||
// on your own.
|
||||
func (c *Caller) remove(cuid string) (success bool) {
|
||||
func (c *Caller) remove(cuid string) (ok 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
|
||||
var hs cmap.ConcurrentMap[string, Handler]
|
||||
hs, ok = c.external.cm.Get(cmd)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Check to see if it's actually a registered handler.
|
||||
if _, ok := c.external[cmd][uid]; !ok {
|
||||
return false
|
||||
if _, ok = hs.Get(cuid); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(c.external[cmd], uid)
|
||||
hs.Remove(uid)
|
||||
c.debug.Printf("removed handler %s", cuid)
|
||||
|
||||
// Assume success.
|
||||
@ -312,9 +346,8 @@ func (c *Caller) remove(cuid string) (success bool) {
|
||||
// the Caller mutex.
|
||||
func (c *Caller) sregister(internal, bg bool, cmd string, handler Handler) (cuid string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
cuid = c.register(internal, bg, cmd, handler)
|
||||
c.mu.Unlock()
|
||||
|
||||
return cuid
|
||||
}
|
||||
|
||||
@ -331,22 +364,29 @@ func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid
|
||||
cuid += ":bg"
|
||||
}
|
||||
|
||||
var (
|
||||
parent *nestedHandlers
|
||||
chandlers cmap.ConcurrentMap[string, Handler]
|
||||
ok bool
|
||||
)
|
||||
|
||||
if internal {
|
||||
if _, ok := c.internal[cmd]; !ok {
|
||||
c.internal[cmd] = map[string]Handler{}
|
||||
}
|
||||
|
||||
c.internal[cmd][uid] = handler
|
||||
parent = c.internal
|
||||
} else {
|
||||
if _, ok := c.external[cmd]; !ok {
|
||||
c.external[cmd] = map[string]Handler{}
|
||||
}
|
||||
|
||||
c.external[cmd][uid] = handler
|
||||
parent = c.external
|
||||
}
|
||||
|
||||
_, file, line, _ := runtime.Caller(3)
|
||||
chandlers, ok = parent.cm.Get(cmd)
|
||||
|
||||
if !ok {
|
||||
chandlers = cmap.New[Handler]()
|
||||
}
|
||||
|
||||
chandlers.Set(uid, handler)
|
||||
|
||||
parent.cm.Set(cmd, chandlers)
|
||||
|
||||
_, file, line, _ := runtime.Caller(2)
|
||||
c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line)
|
||||
|
||||
return cuid
|
||||
@ -458,7 +498,6 @@ func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
|
||||
}
|
||||
|
||||
client.Config.RecoverFunc(client, err)
|
||||
return
|
||||
}
|
||||
|
||||
// HandlerError is the error returned when a panic is intentionally recovered
|
||||
@ -495,6 +534,8 @@ func (e *HandlerError) String() string {
|
||||
// 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.
|
||||
//
|
||||
//goland:noinspection GoUnusedExportedFunction
|
||||
func DefaultRecoverHandler(client *Client, err *HandlerError) {
|
||||
if client.Config.Debug == nil {
|
||||
fmt.Println(err.Error())
|
||||
|
30
handler_test.go
Normal file
30
handler_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package girc
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCaller_AddHandler(t *testing.T) {
|
||||
var passChan = make(chan struct{})
|
||||
nullClient := &Client{mu: sync.RWMutex{}}
|
||||
c := newCaller(nullClient, log.Default())
|
||||
c.AddBg("PRIVMSG", func(c *Client, e Event) {
|
||||
passChan <- struct{}{}
|
||||
})
|
||||
|
||||
go func() {
|
||||
c.exec("PRIVMSG", true, nullClient, &Event{})
|
||||
}()
|
||||
|
||||
if c.external.lenFor("JONES") != 0 {
|
||||
t.Fatalf("wanted %d handlers, got %d", 0, c.internal.lenFor("JONES"))
|
||||
}
|
||||
|
||||
if c.external.lenFor("PRIVMSG") != 1 {
|
||||
t.Fatalf("wanted %d handlers, got %d", 1, c.external.lenFor("PRIVMSG"))
|
||||
}
|
||||
|
||||
<-passChan
|
||||
}
|
82
modes.go
82
modes.go
@ -7,7 +7,8 @@ package girc
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
)
|
||||
|
||||
// CMode represents a single step of a given mode change.
|
||||
@ -118,13 +119,14 @@ func (c *CModes) Get(mode string) (args string, ok bool) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
@ -157,7 +159,7 @@ func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) {
|
||||
// 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
|
||||
var newcm []CMode
|
||||
|
||||
for j := 0; j < len(c.modes); j++ {
|
||||
isin := false
|
||||
@ -166,14 +168,14 @@ func (c *CModes) Apply(modes []CMode) {
|
||||
continue
|
||||
}
|
||||
if c.modes[j].name == modes[i].name && modes[i].add {
|
||||
new = append(new, modes[i])
|
||||
newcm = append(newcm, modes[i])
|
||||
isin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isin {
|
||||
new = append(new, c.modes[j])
|
||||
newcm = append(newcm, c.modes[j])
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,19 +185,19 @@ func (c *CModes) Apply(modes []CMode) {
|
||||
}
|
||||
|
||||
isin := false
|
||||
for j := 0; j < len(new); j++ {
|
||||
if modes[i].name == new[j].name {
|
||||
for j := 0; j < len(newcm); j++ {
|
||||
if modes[i].name == newcm[j].name {
|
||||
isin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isin {
|
||||
new = append(new, modes[i])
|
||||
newcm = append(newcm, modes[i])
|
||||
}
|
||||
}
|
||||
|
||||
c.modes = new
|
||||
c.modes = newcm
|
||||
}
|
||||
|
||||
// Parse parses a set of flags and args, returning the necessary list of
|
||||
@ -333,10 +335,9 @@ func handleMODE(c *Client, e Event) {
|
||||
return
|
||||
}
|
||||
|
||||
c.state.RLock()
|
||||
channel := c.state.lookupChannel(e.Params[0])
|
||||
if channel == nil {
|
||||
c.state.RUnlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -363,17 +364,15 @@ func handleMODE(c *Client, e Event) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if validmodes, ok := s.serverOptions.Get("CHANMODES"); ok && IsValidChannelMode(validmodes) {
|
||||
return validmodes
|
||||
}
|
||||
|
||||
return ModeDefaults
|
||||
}
|
||||
|
||||
@ -381,64 +380,47 @@ func (s *state) chanModes() string {
|
||||
// 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) {
|
||||
if prefix, ok := s.serverOptions.Get("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
|
||||
channels cmap.ConcurrentMap[string, *Perms]
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the channel permissions.
|
||||
func (p *UserPerms) Copy() (perms *UserPerms) {
|
||||
np := &UserPerms{
|
||||
channels: make(map[string]Perms),
|
||||
channels: cmap.New[*Perms](),
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
for key := range p.channels {
|
||||
np.channels[key] = p.channels[key]
|
||||
for tuple := range p.channels.IterBuffered() {
|
||||
np.channels.Set(tuple.Key, tuple.Val)
|
||||
}
|
||||
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) Lookup(channel string) (perms *Perms, ok bool) {
|
||||
return p.channels.Get(ToRFC1459(channel))
|
||||
}
|
||||
|
||||
func (p *UserPerms) set(channel string, perms Perms) {
|
||||
p.mu.Lock()
|
||||
p.channels[ToRFC1459(channel)] = perms
|
||||
p.mu.Unlock()
|
||||
func (p *UserPerms) set(channel string, perms *Perms) {
|
||||
p.channels.Set(ToRFC1459(channel), perms)
|
||||
}
|
||||
|
||||
func (p *UserPerms) remove(channel string) {
|
||||
p.mu.Lock()
|
||||
delete(p.channels, ToRFC1459(channel))
|
||||
p.mu.Unlock()
|
||||
p.channels.Remove(ToRFC1459(channel))
|
||||
}
|
||||
|
||||
// Perms contains all channel-based user permissions. The minimum op, and
|
||||
@ -464,7 +446,7 @@ type Perms struct {
|
||||
|
||||
// IsAdmin indicates that the user has banning abilities, and are likely a
|
||||
// very trustable user (e.g. op+).
|
||||
func (m Perms) IsAdmin() bool {
|
||||
func (m *Perms) IsAdmin() bool {
|
||||
if m.Owner || m.Admin || m.Op {
|
||||
return true
|
||||
}
|
||||
@ -474,7 +456,7 @@ func (m Perms) IsAdmin() bool {
|
||||
|
||||
// IsTrusted indicates that the user at least has modes set upon them, higher
|
||||
// than a regular joining user.
|
||||
func (m Perms) IsTrusted() bool {
|
||||
func (m *Perms) IsTrusted() bool {
|
||||
if m.IsAdmin() || m.HalfOp || m.Voice {
|
||||
return true
|
||||
}
|
||||
|
355
state.go
355
state.go
@ -6,24 +6,29 @@ package girc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
)
|
||||
|
||||
// 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
|
||||
*sync.RWMutex
|
||||
// nick, ident, and host are the internal trackers for our user.
|
||||
nick, ident, host string
|
||||
nick, ident, host atomic.Value
|
||||
// channels represents all channels we're active in.
|
||||
channels map[string]*Channel
|
||||
// channels map[string]*Channel
|
||||
channels cmap.ConcurrentMap[string, *Channel]
|
||||
// users represents all of users that we're tracking.
|
||||
users map[string]*User
|
||||
// users map[string]*User
|
||||
users cmap.ConcurrentMap[string, *User]
|
||||
// enabledCap are the capabilities which are enabled for this connection.
|
||||
enabledCap map[string]map[string]string
|
||||
// enabledCap map[string]map[string]string
|
||||
enabledCap cmap.ConcurrentMap[string, map[string]string]
|
||||
// tmpCap are the capabilties which we share with the server during the
|
||||
// last capability check. These will get sent once we have received the
|
||||
// last capability list command from the server.
|
||||
@ -31,10 +36,18 @@ type state struct {
|
||||
// serverOptions are the standard capabilities and configurations
|
||||
// supported by the server at connection time. This also includes
|
||||
// RPL_ISUPPORT entries.
|
||||
serverOptions map[string]string
|
||||
// serverOptions map[string]string
|
||||
serverOptions cmap.ConcurrentMap[string, string]
|
||||
|
||||
// network is an alternative way to store and retrieve the NETWORK server option.
|
||||
network atomic.Value
|
||||
|
||||
// motd is the servers message of the day.
|
||||
motd string
|
||||
|
||||
// client is a useful pointer to the state's related Client instance.
|
||||
client *Client
|
||||
|
||||
// sts are strict transport security configurations, if specified by the
|
||||
// server.
|
||||
//
|
||||
@ -43,44 +56,69 @@ type state struct {
|
||||
sts strictTransport
|
||||
}
|
||||
|
||||
type Clearer interface {
|
||||
Clear()
|
||||
}
|
||||
|
||||
// reset resets the state back to it's original form.
|
||||
func (s *state) reset(initial bool) {
|
||||
s.Lock()
|
||||
s.nick = ""
|
||||
s.ident = ""
|
||||
s.host = ""
|
||||
s.channels = make(map[string]*Channel)
|
||||
s.users = make(map[string]*User)
|
||||
s.serverOptions = make(map[string]string)
|
||||
s.enabledCap = make(map[string]map[string]string)
|
||||
s.nick.Store("")
|
||||
s.ident.Store("")
|
||||
s.host.Store("")
|
||||
s.network.Store("")
|
||||
var cmaps = []Clearer{&s.channels, &s.users, &s.serverOptions}
|
||||
for i, cm := range cmaps {
|
||||
switch {
|
||||
case i == 0 && initial:
|
||||
cm = cmap.New[*Channel]()
|
||||
case i == 1 && initial:
|
||||
cm = cmap.New[*User]()
|
||||
case i == 2 && initial:
|
||||
cm = cmap.New[string]()
|
||||
default:
|
||||
cm.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
s.enabledCap = cmap.New[map[string]string]()
|
||||
s.tmpCap = make(map[string]map[string]string)
|
||||
s.motd = ""
|
||||
|
||||
if initial {
|
||||
s.sts.reset()
|
||||
}
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// User represents an IRC user and the state attached to them.
|
||||
type User struct {
|
||||
// Nick is the users current nickname. rfc1459 compliant.
|
||||
Nick string `json:"nick"`
|
||||
Nick *MarshalableAtomicValue `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"`
|
||||
Ident *MarshalableAtomicValue `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"`
|
||||
|
||||
// Mask is the combined Nick!Ident@Host of the given user.
|
||||
Mask *MarshalableAtomicValue `json:"mask"`
|
||||
|
||||
// Network is the name of the IRC network where this user was found.
|
||||
// This has been added for the purposes of girc being used in multi-client scenarios with data persistence.
|
||||
Network string `json:"network"`
|
||||
|
||||
// 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"`
|
||||
//
|
||||
// NOTE: If the ChannelList is empty for the user, then the user's info could be out of date.
|
||||
// turns out Concurrent-Map implements json.Marhsal!
|
||||
// https://github.com/orcaman/concurrent-map/v2/blob/893feb299719d9cbb2cfbe08b6dd4eb567d8039d/concurrent_map.go#L305
|
||||
ChannelList cmap.ConcurrentMap[string, *Channel] `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.
|
||||
@ -94,6 +132,8 @@ type User struct {
|
||||
// channel. This supports non-rfc style modes like Admin, Owner, and HalfOp.
|
||||
Perms *UserPerms `json:"perms"`
|
||||
|
||||
Stale bool
|
||||
|
||||
// Extras are things added on by additional tracking methods, which may
|
||||
// or may not work on the IRC server in mention.
|
||||
Extras struct {
|
||||
@ -114,24 +154,26 @@ type User struct {
|
||||
} `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 {
|
||||
// Channels returns a slice of pointers to Channel types that the client knows the user is in.
|
||||
func (u *User) Channels(c *Client) []*Channel {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
channels := []*Channel{}
|
||||
var channels []*Channel
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(u.ChannelList); i++ {
|
||||
ch := c.state.lookupChannel(u.ChannelList[i])
|
||||
for listed := range u.ChannelList.IterBuffered() {
|
||||
chn := listed.Val
|
||||
if chn != nil {
|
||||
channels = append(channels, chn)
|
||||
continue
|
||||
}
|
||||
ch := c.state.lookupChannel(listed.Key)
|
||||
if ch != nil {
|
||||
u.ChannelList.Set(listed.Key, ch)
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return channels
|
||||
}
|
||||
@ -147,53 +189,45 @@ func (u *User) Copy() *User {
|
||||
*nu = *u
|
||||
|
||||
nu.Perms = u.Perms.Copy()
|
||||
_ = copy(nu.ChannelList, u.ChannelList)
|
||||
for ch := range u.ChannelList.IterBuffered() {
|
||||
nu.ChannelList.Set(ch.Key, ch.Val)
|
||||
}
|
||||
|
||||
return nu
|
||||
}
|
||||
|
||||
// addChannel adds the channel to the users channel list.
|
||||
func (u *User) addChannel(name string) {
|
||||
func (u *User) addChannel(name string, chn *Channel) {
|
||||
name = ToRFC1459(name)
|
||||
|
||||
if u.InChannel(name) {
|
||||
return
|
||||
}
|
||||
|
||||
u.ChannelList = append(u.ChannelList, ToRFC1459(name))
|
||||
sort.Strings(u.ChannelList)
|
||||
if u.ChannelList.Has(name) {
|
||||
return
|
||||
}
|
||||
|
||||
u.Perms.set(name, Perms{})
|
||||
u.ChannelList.Set(name, chn)
|
||||
|
||||
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.ChannelList.Remove(name)
|
||||
|
||||
u.Perms.remove(name)
|
||||
}
|
||||
|
||||
// InChannel checks to see if a user is in the given channel.
|
||||
// Maybe don't rely on it though, hasn't been the same since the war. :^)
|
||||
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
|
||||
return u.ChannelList.Has(name)
|
||||
}
|
||||
|
||||
// Lifetime represents the amount of time that has passed since we have first
|
||||
@ -219,10 +253,16 @@ type Channel struct {
|
||||
Name string `json:"name"`
|
||||
// Topic of the channel.
|
||||
Topic string `json:"topic"`
|
||||
|
||||
// Created is the time/date the channel was created (if available).
|
||||
// Created time.Time `json:"created"`
|
||||
// TODO: Figure out if these are all unix timestamps, if so, convert it to time.Time
|
||||
Created string `json:"created"`
|
||||
// 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"`
|
||||
// the channel. Each is the1 nickname, and is rfc1459 compliant.
|
||||
UserList cmap.ConcurrentMap[string, *User] `json:"user_list"`
|
||||
// Network is the name of the IRC network where this channel was found.
|
||||
// This has been added for the purposes of girc being used in multi-client scenarios with data persistence.
|
||||
Network string `json:"network"`
|
||||
// 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.
|
||||
@ -231,37 +271,35 @@ type Channel struct {
|
||||
|
||||
// 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 {
|
||||
func (ch *Channel) Users(c *Client) []*User {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
var users []*User
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
user := c.state.lookupUser(ch.UserList[i])
|
||||
for listed := range ch.UserList.IterBuffered() {
|
||||
user := c.state.lookupUser(listed.Key)
|
||||
if user != nil {
|
||||
ch.UserList.Set(listed.Key, user)
|
||||
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 {
|
||||
func (ch *Channel) Trusted(c *Client) []*User {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
var users []*User
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
user := c.state.lookupUser(ch.UserList[i])
|
||||
for listed := range ch.UserList.IterBuffered() {
|
||||
user := c.state.lookupUser(listed.Key)
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
@ -271,7 +309,6 @@ func (ch Channel) Trusted(c *Client) []*User {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return users
|
||||
}
|
||||
@ -279,55 +316,44 @@ func (ch Channel) Trusted(c *Client) []*User {
|
||||
// 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 {
|
||||
func (ch *Channel) Admins(c *Client) []*User {
|
||||
if c == nil {
|
||||
panic("nil Client provided")
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
var users []*User
|
||||
|
||||
c.state.RLock()
|
||||
for i := 0; i < len(ch.UserList); i++ {
|
||||
user := c.state.lookupUser(ch.UserList[i])
|
||||
if user == nil {
|
||||
continue
|
||||
for listed := range ch.UserList.IterBuffered() {
|
||||
ui := listed.Val
|
||||
|
||||
if ui == nil {
|
||||
if ui = c.state.lookupUser(listed.Key); ui == nil {
|
||||
continue
|
||||
}
|
||||
ch.UserList.Set(listed.Key, ui)
|
||||
}
|
||||
|
||||
perms, ok := user.Perms.Lookup(ch.Name)
|
||||
perms, ok := ui.Perms.Lookup(ch.Name)
|
||||
if ok && perms.IsAdmin() {
|
||||
users = append(users, user)
|
||||
users = append(users, ui)
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// addUser adds a user to the users list.
|
||||
func (ch *Channel) addUser(nick string) {
|
||||
func (ch *Channel) addUser(nick string, usr *User) {
|
||||
if ch.UserIn(nick) {
|
||||
return
|
||||
}
|
||||
|
||||
ch.UserList = append(ch.UserList, ToRFC1459(nick))
|
||||
sort.Strings(ch.UserList)
|
||||
ch.UserList.Set(ToRFC1459(nick), usr)
|
||||
}
|
||||
|
||||
// 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:]...)
|
||||
}
|
||||
ch.UserList.Remove(nick)
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of a given channel.
|
||||
@ -339,7 +365,9 @@ func (ch *Channel) Copy() *Channel {
|
||||
nc := &Channel{}
|
||||
*nc = *ch
|
||||
|
||||
_ = copy(nc.UserList, ch.UserList)
|
||||
for v := range ch.UserList.IterBuffered() {
|
||||
nc.UserList.Set(v.Val.Nick.Load().(string), v.Val)
|
||||
}
|
||||
|
||||
// And modes.
|
||||
nc.Modes = ch.Modes.Copy()
|
||||
@ -349,20 +377,13 @@ func (ch *Channel) Copy() *Channel {
|
||||
|
||||
// Len returns the count of users in a given channel.
|
||||
func (ch *Channel) Len() int {
|
||||
return len(ch.UserList)
|
||||
return ch.UserList.Count()
|
||||
}
|
||||
|
||||
// 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
|
||||
return ch.UserList.Has(name)
|
||||
}
|
||||
|
||||
// Lifetime represents the amount of time that has passed since we have first
|
||||
@ -373,19 +394,21 @@ func (ch *Channel) Lifetime() time.Duration {
|
||||
|
||||
// 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 {
|
||||
if _, ok := s.channels.Get(ToRFC1459(name)); ok {
|
||||
return false
|
||||
}
|
||||
|
||||
s.channels[ToRFC1459(name)] = &Channel{
|
||||
s.channels.Set(ToRFC1459(name), &Channel{
|
||||
Name: name,
|
||||
UserList: []string{},
|
||||
UserList: cmap.New[*User](),
|
||||
Joined: time.Now(),
|
||||
Network: s.client.NetworkName(),
|
||||
Modes: NewCModes(supported, prefixes),
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
@ -394,69 +417,92 @@ func (s *state) createChannel(name string) (ok bool) {
|
||||
func (s *state) deleteChannel(name string) {
|
||||
name = ToRFC1459(name)
|
||||
|
||||
_, ok := s.channels[name]
|
||||
chn, ok := s.channels.Get(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)
|
||||
for listed := range chn.UserList.IterBuffered() {
|
||||
usr, uok := s.users.Get(listed.Key)
|
||||
if uok {
|
||||
usr.deleteChannel(name)
|
||||
}
|
||||
}
|
||||
|
||||
delete(s.channels, name)
|
||||
s.channels.Remove(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)]
|
||||
ci, cok := s.channels.Get(ToRFC1459(name))
|
||||
if ci == nil || !cok {
|
||||
return nil
|
||||
}
|
||||
return ci
|
||||
}
|
||||
|
||||
// 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)]
|
||||
usr, uok := s.users.Get(ToRFC1459(name))
|
||||
if usr == nil || !uok {
|
||||
return nil
|
||||
}
|
||||
return usr
|
||||
}
|
||||
|
||||
// createUser creates the user in state, if not already done.
|
||||
func (s *state) createUser(src *Source) (ok bool) {
|
||||
if _, ok := s.users[src.ID()]; ok {
|
||||
func (s *state) createUser(src *Source) (u *User, ok bool) {
|
||||
if u, ok = s.users.Get(src.ID()); ok {
|
||||
// User already exists.
|
||||
return false
|
||||
return u, false
|
||||
}
|
||||
|
||||
s.users[src.ID()] = &User{
|
||||
Nick: src.Name,
|
||||
Host: src.Host,
|
||||
Ident: src.Ident,
|
||||
FirstSeen: time.Now(),
|
||||
LastActive: time.Now(),
|
||||
Perms: &UserPerms{channels: make(map[string]Perms)},
|
||||
mask := strs.Get()
|
||||
if src.Name != "" {
|
||||
mask.MustWriteString(src.Name)
|
||||
}
|
||||
_ = mask.WriteByte('!')
|
||||
if src.Ident != "" {
|
||||
mask.MustWriteString(src.Ident)
|
||||
}
|
||||
_ = mask.WriteByte('@')
|
||||
if src.Host != "" {
|
||||
mask.MustWriteString(src.Host)
|
||||
}
|
||||
|
||||
return true
|
||||
u = &User{
|
||||
Nick: NewAtomicString(src.Name),
|
||||
Host: src.Host,
|
||||
Ident: NewAtomicString(src.Ident),
|
||||
Mask: NewAtomicString(mask.String()),
|
||||
ChannelList: cmap.New[*Channel](),
|
||||
FirstSeen: time.Now(),
|
||||
LastActive: time.Now(),
|
||||
Network: s.client.NetworkName(),
|
||||
Perms: &UserPerms{channels: cmap.New[*Perms]()},
|
||||
}
|
||||
|
||||
strs.MustPut(mask)
|
||||
|
||||
s.users.Set(src.ID(), u)
|
||||
return u, true
|
||||
}
|
||||
|
||||
// deleteUser removes the user from channel state.
|
||||
func (s *state) deleteUser(channelName, nick string) {
|
||||
user := s.lookupUser(nick)
|
||||
if user == nil {
|
||||
s.client.debug.Printf(nick + ": was not found when trying to deleteUser from " + channelName)
|
||||
return
|
||||
}
|
||||
|
||||
if channelName == "" {
|
||||
for i := 0; i < len(user.ChannelList); i++ {
|
||||
s.channels[user.ChannelList[i]].deleteUser(nick)
|
||||
}
|
||||
|
||||
delete(s.users, ToRFC1459(nick))
|
||||
user.ChannelList.Clear()
|
||||
// While we do still want to remove them from the channels,
|
||||
// We want to hold onto that user object regardless on if they dip-set.
|
||||
// s.users.Remove(ToRFC1459(nick))
|
||||
user.Stale = true
|
||||
return
|
||||
}
|
||||
|
||||
@ -467,12 +513,8 @@ func (s *state) deleteUser(channelName, nick string) {
|
||||
|
||||
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))
|
||||
if user.ChannelList.Count() == 0 {
|
||||
user.Stale = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,29 +523,32 @@ func (s *state) renameUser(from, to string) {
|
||||
from = ToRFC1459(from)
|
||||
|
||||
// Update our nickname.
|
||||
if from == ToRFC1459(s.nick) {
|
||||
s.nick = to
|
||||
if from == ToRFC1459(s.nick.Load().(string)) {
|
||||
s.nick.Store(to)
|
||||
}
|
||||
|
||||
user := s.lookupUser(from)
|
||||
if user == nil {
|
||||
|
||||
old, oldok := s.users.Pop(from)
|
||||
if !oldok && user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
delete(s.users, from)
|
||||
if old != nil && user == nil {
|
||||
user = old
|
||||
}
|
||||
|
||||
user.Nick = to
|
||||
user.Nick.Store(to)
|
||||
user.LastActive = time.Now()
|
||||
s.users[ToRFC1459(to)] = user
|
||||
s.users.Set(ToRFC1459(to), user)
|
||||
|
||||
for i := 0; i < len(user.ChannelList); i++ {
|
||||
for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ {
|
||||
if s.channels[user.ChannelList[i]].UserList[j] == from {
|
||||
s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to)
|
||||
|
||||
sort.Strings(s.channels[user.ChannelList[i]].UserList)
|
||||
break
|
||||
}
|
||||
for chanchan := range s.channels.IterBuffered() {
|
||||
chn := chanchan.Val
|
||||
if chn == nil {
|
||||
continue
|
||||
}
|
||||
if old, oldok = chn.UserList.Pop(from); oldok {
|
||||
chn.UserList.Set(to, old)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
218
state_test.go
218
state_test.go
@ -29,30 +29,30 @@ const mockConnStartState = `:dummy.int NOTICE * :*** Looking up your hostname...
|
||||
:dummy.int NOTICE * :*** Checking Ident
|
||||
:dummy.int NOTICE * :*** Found your hostname
|
||||
:dummy.int NOTICE * :*** No Ident response
|
||||
:dummy.int 001 nick :Welcome to the DUMMY Internet Relay Chat Network nick
|
||||
:dummy.int 005 nick NETWORK=DummyIRC NICKLEN=20 :are supported by this server
|
||||
:dummy.int 375 nick :- dummy.int Message of the Day -
|
||||
:dummy.int 372 nick :example motd
|
||||
:dummy.int 376 nick :End of /MOTD command.
|
||||
:nick!~user@local.int JOIN #channel * :realname
|
||||
:dummy.int 332 nick #channel :example topic
|
||||
:dummy.int 353 nick = #channel :nick!~user@local.int @nick2!nick2@other.int
|
||||
:dummy.int 366 nick #channel :End of /NAMES list.
|
||||
:dummy.int 354 nick 1 #channel ~user local.int nick 0 :realname
|
||||
:dummy.int 354 nick 1 #channel nick2 other.int nick2 nick2 :realname2
|
||||
:dummy.int 315 nick #channel :End of /WHO list.
|
||||
:nick!~user@local.int JOIN #channel2 * :realname
|
||||
:dummy.int 332 nick #channel2 :example topic
|
||||
:dummy.int 353 nick = #channel2 :nick!~user@local.int @nick2!nick2@other.int
|
||||
:dummy.int 366 nick #channel2 :End of /NAMES list.
|
||||
:dummy.int 354 nick 1 #channel2 ~user local.int nick 0 :realname
|
||||
:dummy.int 354 nick 1 #channel2 nick2 other.int nick2 nick2 :realname2
|
||||
:dummy.int 315 nick #channel2 :End of /WHO list.
|
||||
:dummy.int 001 fhjones :Welcome to the DUMMY Internet Relay Chat Network fhjones
|
||||
:dummy.int 005 fhjones NETWORK=DummyIRC NICKLEN=20 :are supported by this server
|
||||
:dummy.int 375 fhjones :- dummy.int Message of the Day -
|
||||
:dummy.int 372 fhjones :example motd
|
||||
:dummy.int 376 fhjones :End of /MOTD command.
|
||||
:fhjones!~user@local.int JOIN #channel * :realname
|
||||
:dummy.int 332 fhjones #channel :example topic
|
||||
:dummy.int 353 fhjones = #channel :fhjones!~user@local.int @nick2!nick2@other.int
|
||||
:dummy.int 366 fhjones #channel :End of /NAMES list.
|
||||
:dummy.int 354 fhjones 1 #channel ~user local.int fhjones 0 :realname
|
||||
:dummy.int 354 fhjones 1 #channel nick2 other.int nick2 nick2 :realname2
|
||||
:dummy.int 315 fhjones #channel :End of /WHO list.
|
||||
:fhjones!~user@local.int JOIN #channel2 * :realname
|
||||
:dummy.int 332 fhjones #channel2 :example topic
|
||||
:dummy.int 353 fhjones = #channel2 :fhjones!~user@local.int @nick2!nick2@other.int
|
||||
:dummy.int 366 fhjones #channel2 :End of /NAMES list.
|
||||
:dummy.int 354 fhjones 1 #channel2 ~user local.int fhjones 0 :realname
|
||||
:dummy.int 354 fhjones 1 #channel2 nick2 other.int nick2 nick2 :realname2
|
||||
:dummy.int 315 fhjones #channel2 :End of /WHO list.
|
||||
`
|
||||
|
||||
const mockConnEndState = `:nick2!nick2@other.int QUIT :example reason
|
||||
:nick!~user@local.int PART #channel2 :example reason
|
||||
:nick!~user@local.int NICK newnick
|
||||
:fhjones!~user@local.int PART #channel2 :example reason
|
||||
:fhjones!~user@local.int NICK notjones
|
||||
`
|
||||
|
||||
func TestState(t *testing.T) {
|
||||
@ -71,123 +71,176 @@ func TestState(t *testing.T) {
|
||||
finishStart := make(chan bool, 1)
|
||||
go debounce(250*time.Millisecond, bounceStart, func() {
|
||||
if motd := c.ServerMOTD(); motd != "example motd" {
|
||||
t.Fatalf("Client.ServerMOTD() returned invalid MOTD: %q", motd)
|
||||
t.Errorf("Client.ServerMOTD() returned invalid MOTD: %q", motd)
|
||||
return
|
||||
}
|
||||
|
||||
if network := c.NetworkName(); network != "DummyIRC" {
|
||||
t.Fatalf("Client.NetworkName() returned invalid network name: %q", network)
|
||||
network := c.NetworkName()
|
||||
|
||||
if network != "DummyIRC" && network != "DUMMY" {
|
||||
t.Errorf("User.Network == %q, want \"DummyIRC\" or \"DUMMY\"", network)
|
||||
return
|
||||
}
|
||||
|
||||
if caseExample, ok := c.GetServerOption("NICKLEN"); !ok || caseExample != "20" {
|
||||
t.Fatalf("Client.GetServerOptions returned invalid ISUPPORT variable")
|
||||
t.Logf("successfully tested network name: %s", network)
|
||||
|
||||
caseExample, ok := c.GetServerOpt("NICKLEN")
|
||||
|
||||
if !ok || caseExample != "20" {
|
||||
t.Errorf("Client.GetServerOptions returned invalid ISUPPORT variable: %q", caseExample)
|
||||
}
|
||||
|
||||
t.Logf("successfully serveroption NICKLEN: %s", caseExample)
|
||||
|
||||
users := c.UserList()
|
||||
channels := c.ChannelList()
|
||||
|
||||
if !reflect.DeepEqual(users, []string{"nick", "nick2"}) {
|
||||
if !reflect.DeepEqual(users, []string{"fhjones", "nick2"}) {
|
||||
// This could fail too, if sorting isn't occurring.
|
||||
t.Fatalf("got state users %#v, wanted: %#v", users, []string{"nick", "nick2"})
|
||||
t.Errorf("got state users %#v, wanted: %#v", users, []string{"fhjones", "nick2"})
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("successfully checked userlist: %v", users)
|
||||
|
||||
if !reflect.DeepEqual(channels, []string{"#channel", "#channel2"}) {
|
||||
// This could fail too, if sorting isn't occurring.
|
||||
t.Fatalf("got state channels %#v, wanted: %#v", channels, []string{"#channel", "#channel2"})
|
||||
t.Errorf("got state channels %#v, wanted: %#v", channels, []string{"#channel", "#channel2"})
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("successfully checked channel list: %v", channels)
|
||||
|
||||
fullChannels := c.Channels()
|
||||
for i := 0; i < len(fullChannels); i++ {
|
||||
if fullChannels[i].Name != channels[i] {
|
||||
t.Fatalf("fullChannels name doesn't map to same name in ChannelsList: %q :: %#v", fullChannels[i].Name, channels)
|
||||
t.Errorf("fullChannels name doesn't map to same name in ChannelsList: %q :: %#v", fullChannels[i].Name, channels)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("successfully checked full channel list: %s: %v", fullChannels[i].Name, channels)
|
||||
}
|
||||
|
||||
fullUsers := c.Users()
|
||||
for i := 0; i < len(fullUsers); i++ {
|
||||
if fullUsers[i].Nick != users[i] {
|
||||
t.Fatalf("fullUsers nick doesn't map to same nick in UsersList: %q :: %#v", fullUsers[i].Nick, users)
|
||||
if fullUsers[i].Nick.Load().(string) != users[i] {
|
||||
t.Errorf("fullUsers nick doesn't map to same nick in UsersList: %q :: %#v", fullUsers[i].Nick, users)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ch := c.LookupChannel("#channel")
|
||||
if ch == nil {
|
||||
t.Fatal("Client.LookupChannel returned nil on existing channel")
|
||||
t.Error("Client.LookupChannel returned nil on existing channel")
|
||||
return
|
||||
}
|
||||
|
||||
adm := ch.Admins(c)
|
||||
admList := []string{}
|
||||
var admList []string
|
||||
for i := 0; i < len(adm); i++ {
|
||||
admList = append(admList, adm[i].Nick)
|
||||
admList = append(admList, adm[i].Nick.Load().(string))
|
||||
}
|
||||
trusted := ch.Trusted(c)
|
||||
trustedList := []string{}
|
||||
var trustedList []string
|
||||
for i := 0; i < len(trusted); i++ {
|
||||
trustedList = append(trustedList, trusted[i].Nick)
|
||||
trustedList = append(trustedList, trusted[i].Nick.Load().(string))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(admList, []string{"nick2"}) {
|
||||
t.Fatalf("got Channel.Admins() == %#v, wanted %#v", admList, []string{"nick2"})
|
||||
t.Errorf("got Channel.Admins() == %#v, wanted %#v", admList, []string{"nick2"})
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(trustedList, []string{"nick2"}) {
|
||||
t.Fatalf("got Channel.Trusted() == %#v, wanted %#v", trustedList, []string{"nick2"})
|
||||
t.Errorf("got Channel.Trusted() == %#v, wanted %#v", trustedList, []string{"nick2"})
|
||||
return
|
||||
}
|
||||
|
||||
if topic := ch.Topic; topic != "example topic" {
|
||||
t.Fatalf("Channel.Topic == %q, want \"example topic\"", topic)
|
||||
t.Errorf("Channel.Topic == %q, want \"example topic\"", topic)
|
||||
return
|
||||
}
|
||||
|
||||
if in := ch.UserIn("nick"); !in {
|
||||
t.Fatalf("Channel.UserIn == %t, want %t", in, true)
|
||||
if ch.Network != "DummyIRC" && ch.Network != "DUMMY" {
|
||||
t.Errorf("Channel.Network == %q, want \"DummyIRC\" or \"DUMMY\"", ch.Network)
|
||||
return
|
||||
}
|
||||
|
||||
if in := ch.UserIn("fhjones"); !in {
|
||||
t.Errorf("Channel.UserIn == %t, want %t", in, true)
|
||||
return
|
||||
}
|
||||
|
||||
if users := ch.Users(c); len(users) != 2 {
|
||||
t.Fatalf("Channel.Users == %#v, wanted length of 2", users)
|
||||
t.Errorf("Channel.Users == %#v, wanted length of 2", users)
|
||||
return
|
||||
}
|
||||
|
||||
if h := c.GetHost(); h != "local.int" {
|
||||
t.Fatalf("Client.GetHost() == %q, want local.int", h)
|
||||
t.Errorf("Client.GetHost() == %q, want local.int", h)
|
||||
return
|
||||
}
|
||||
|
||||
if nick := c.GetNick(); nick != "nick" {
|
||||
t.Fatalf("Client.GetNick() == %q, want nick", nick)
|
||||
if nick := c.GetNick(); nick != "fhjones" {
|
||||
t.Errorf("Client.GetNick() == %q, want nick", nick)
|
||||
return
|
||||
}
|
||||
|
||||
if ident := c.GetIdent(); ident != "~user" {
|
||||
t.Fatalf("Client.GetIdent() == %q, want ~user", ident)
|
||||
t.Errorf("Client.GetIdent() == %q, want ~user", ident)
|
||||
return
|
||||
}
|
||||
|
||||
user := c.LookupUser("nick")
|
||||
user := c.LookupUser("fhjones")
|
||||
if user == nil {
|
||||
t.Fatal("Client.LookupUser() returned nil on existing user")
|
||||
t.Error("Client.LookupUser() returned nil on existing user")
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(user.ChannelList, []string{"#channel", "#channel2"}) {
|
||||
t.Fatalf("User.ChannelList == %#v, wanted %#v", user.ChannelList, []string{"#channel", "#channel2"})
|
||||
if user.ChannelList.Count() != len([]string{"#channel", "#channel2"}) {
|
||||
t.Errorf("user.ChannelList.Count() == %d, wanted %d",
|
||||
user.ChannelList.Count(), len([]string{"#channel", "#channel2"}))
|
||||
return
|
||||
}
|
||||
|
||||
if !user.ChannelList.Has("#channel") || !user.ChannelList.Has("#channel2") {
|
||||
t.Errorf("channel list is missing either #channel or #channel2")
|
||||
return
|
||||
}
|
||||
|
||||
if count := len(user.Channels(c)); count != 2 {
|
||||
t.Fatalf("len(User.Channels) == %d, want 2", count)
|
||||
t.Errorf("len(User.Channels) == %d, want 2", count)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Nick != "nick" {
|
||||
t.Fatalf("User.Nick == %q, wanted \"nick\"", user.Nick)
|
||||
if user.Nick.Load().(string) != "fhjones" {
|
||||
t.Errorf("User.Nick == %q, wanted \"nick\"", user.Nick)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Extras.Name != "realname" {
|
||||
t.Fatalf("User.Extras.Name == %q, wanted \"realname\"", user.Extras.Name)
|
||||
t.Errorf("User.Extras.Name == %q, wanted \"realname\"", user.Extras.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Host != "local.int" {
|
||||
t.Fatalf("User.Host == %q, wanted \"local.int\"", user.Host)
|
||||
t.Errorf("User.Host == %q, wanted \"local.int\"", user.Host)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Ident != "~user" {
|
||||
t.Fatalf("User.Ident == %q, wanted \"~user\"", user.Ident)
|
||||
if user.Ident.Load().(string) != "~user" {
|
||||
t.Errorf("User.Ident == %q, wanted \"~user\"", user.Ident)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Network != "DummyIRC" && user.Network != "DUMMY" {
|
||||
t.Errorf("User.Network == %q, want \"DummyIRC\" or \"DUMMY\"", user.Network)
|
||||
return
|
||||
}
|
||||
|
||||
if !user.InChannel("#channel2") {
|
||||
t.Fatal("User.InChannel() returned false for existing channel")
|
||||
t.Error("User.InChannel() returned false for existing channel")
|
||||
return
|
||||
}
|
||||
|
||||
finishStart <- true
|
||||
@ -197,7 +250,9 @@ func TestState(t *testing.T) {
|
||||
bounceStart <- true
|
||||
})
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := conn.Write([]byte(mockConnStartState))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -214,29 +269,44 @@ func TestState(t *testing.T) {
|
||||
finishEnd := make(chan bool, 1)
|
||||
go debounce(250*time.Millisecond, bounceEnd, func() {
|
||||
if !reflect.DeepEqual(c.ChannelList(), []string{"#channel"}) {
|
||||
t.Fatalf("Client.ChannelList() == %#v, wanted %#v", c.ChannelList(), []string{"#channel"})
|
||||
t.Errorf("Client.ChannelList() == %#v, wanted %#v", c.ChannelList(), []string{"#channel"})
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(c.UserList(), []string{"newnick"}) {
|
||||
t.Fatalf("Client.UserList() == %#v, wanted %#v", c.UserList(), []string{"newnick"})
|
||||
if !reflect.DeepEqual(c.UserList(), []string{"notjones"}) {
|
||||
t.Errorf("Client.UserList() == %#v, wanted %#v", c.UserList(), []string{"notjones"})
|
||||
return
|
||||
}
|
||||
|
||||
user := c.LookupUser("newnick")
|
||||
user := c.LookupUser("notjones")
|
||||
if user == nil {
|
||||
t.Fatal("Client.LookupUser() returned nil for existing user")
|
||||
t.Error("Client.LookupUser() returned nil for existing user")
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(user.ChannelList, []string{"#channel"}) {
|
||||
t.Fatalf("user.ChannelList == %q, wanted %q", user.ChannelList, []string{"#channel"})
|
||||
chn, chnok := user.ChannelList.Get("#channel")
|
||||
|
||||
if !chnok {
|
||||
t.Errorf("should have been able to get a pointer by looking up #channel")
|
||||
return
|
||||
}
|
||||
|
||||
channel := c.LookupChannel("#channel")
|
||||
if channel == nil {
|
||||
t.Fatal("Client.LookupChannel() returned nil for existing channel")
|
||||
if chn == nil {
|
||||
t.Error("Client.LookupChannel() returned nil for existing channel")
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(channel.UserList, []string{"newnick"}) {
|
||||
t.Fatalf("channel.UserList == %q, wanted %q", channel.UserList, []string{"newnick"})
|
||||
chn2, _ := user.ChannelList.Get("#channel2")
|
||||
|
||||
if chn2.Len() != len([]string{"notjones"}) {
|
||||
t.Errorf("channel.UserList.Count() == %d, wanted %d",
|
||||
chn2.Len(), len([]string{"notjones"}))
|
||||
return
|
||||
}
|
||||
|
||||
if !chn.UserList.Has("notjones") {
|
||||
t.Errorf("missing notjones from channel.UserList")
|
||||
return
|
||||
}
|
||||
|
||||
finishEnd <- true
|
||||
@ -246,7 +316,9 @@ func TestState(t *testing.T) {
|
||||
bounceEnd <- true
|
||||
})
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if err = conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = conn.Write([]byte(mockConnEndState))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
29
value.go
Normal file
29
value.go
Normal file
@ -0,0 +1,29 @@
|
||||
package girc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type MarshalableAtomicValue struct {
|
||||
*atomic.Value
|
||||
}
|
||||
|
||||
func (m *MarshalableAtomicValue) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("%v", m.Value.Load())), nil
|
||||
}
|
||||
|
||||
func (m *MarshalableAtomicValue) UnmarshalJSON(b []byte) error {
|
||||
m.Value.Store(string(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MarshalableAtomicValue) String() string {
|
||||
return m.Value.Load().(string)
|
||||
}
|
||||
|
||||
func NewAtomicString(s string) *MarshalableAtomicValue {
|
||||
obj := &atomic.Value{}
|
||||
obj.Store(s)
|
||||
return &MarshalableAtomicValue{Value: obj}
|
||||
}
|
4
version.go
Normal file
4
version.go
Normal file
@ -0,0 +1,4 @@
|
||||
package girc
|
||||
|
||||
// Version represents the current library version of girc-atomic.
|
||||
const Version = "v0.5.2"
|
Loading…
Reference in New Issue
Block a user