Compare commits

...

20 Commits
cmap ... master

Author SHA1 Message Date
f9a80d019b
Fix #2 2023-04-27 10:04:53 -07:00
623ca6ffc1
tricking github into knowing what a tag is 2023-03-18 00:11:10 -07:00
568e9652da
Fix race condition 2023-03-17 23:57:45 -07:00
633a5dea16
Overhaul, breaking change in GetPerms 2023-03-17 23:33:19 -07:00
0d38e8f3d9
Update README.md 2022-11-05 00:43:49 -07:00
ee1c72a7f5
Chores: lint, nolint, fmt, etc 2022-10-23 02:01:03 -07:00
5f71c07dee
fmt 2022-10-23 01:56:07 -07:00
d91de1c3c0
Improve tests 2022-10-23 01:56:01 -07:00
3458ae3d6d
Fix pointer receiver inconsistencies 2022-10-23 01:51:42 -07:00
0ae5183ad5
Optimizations stage 1 2022-10-23 01:49:34 -07:00
c1560dfd88
Fix: formatting inconsistencies. Feat: KickBan cmd. 2022-08-17 20:09:25 -07:00
9083cf55cf
Fix: Check for nil from cmap Get 2022-07-14 01:47:52 -07:00
32f2b078c3
Switch UserPerms to cmap and fix panic 2022-07-11 22:36:36 -07:00
61ef265c6c
Not worried about coverage, the sky is falling. 2022-06-25 17:14:29 -07:00
0b5ba7050c
Minor stealth change and backport https://github.com/lrstanley/girc/pull/56 2022-06-25 17:08:40 -07:00
0dec6ca8e2
Coverage: Add silly little test to appease 2022-05-02 22:05:31 -07:00
af4ae39f57
CI: Update go version 2022-05-02 21:25:30 -07:00
18243520dc
Upstream: merge in some changes 2022-05-02 21:15:53 -07:00
f6b2020909
1.18 changes 2022-05-02 20:37:54 -07:00
78e9032f25
Documentation: slight change 2022-04-10 05:25:50 -07:00
31 changed files with 819 additions and 635 deletions

@ -16,7 +16,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18
- name: Build - name: Build
run: go build -v ./... run: go build -v ./...

@ -1,23 +0,0 @@
name: test
on:
push: {}
pull_request: { branches: [master] }
jobs:
test:
runs-on: ubuntu-latest
steps:
- id: goversion
run: |
echo ::set-output name=version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | sed -rn 's/.*"version": "([0-9]\.[0-9]+(\.[0-9]+)?)".*/\1/p' | head -1)
- uses: actions/setup-go@v2
with: { go-version: "${{ steps.goversion.outputs.version }}" }
- uses: actions/checkout@v2
- run: |
go install golang.org/x/lint/golint@latest
- run: $(go env GOPATH)/bin/golint -min_confidence 0.9 -set_exit_status
- run: |
GORACE="exitcode=1 halt_on_error=1" go test -v -coverprofile=coverage.txt -race -timeout 3m -count 3 -cpu 1,4
bash <(curl -s https://codecov.io/bash)
- run: go vet -v .

2
.gitignore vendored

@ -1,2 +1,4 @@
.idea .idea
*.save *.save
*.swp
corpus/

@ -20,7 +20,7 @@
## Features ## Features
- Focuses on ~~simplicity~~ ʀᴀɪɴɪɴɢ ʜᴇʟʟғɪʀᴇ, yet tries to still be flexible. - Focuses on ~~simplicity~~ ʀᴀɪɴɪɴɢ ʜᴇʟʟғɪʀᴇ, yet tries to still be flexible.
- Only requires [standard library packages](https://godoc.org/github.com/yunginnanet/girc-atomic?imports) - 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)!) - 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. - [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). - Support for almost all of the [IRCv3 spec](http://ircv3.net/software/libraries.html).
@ -37,25 +37,11 @@
- Event/message rate limiting. - 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.) - 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)) - CTCP handling and auto-responses ([CTCP](https://godoc.org/github.com/yunginnanet/girc-atomic#CTCP))
- Utilizes the atomic/value package from stdlib to reduce backpressure in multi-client usage. - Utilizes atomics and concurrent maps to reduce backpressure in multi-client usage. (fork)
- Additional CTCP handlers and customization. - Additional CTCP handlers and customization. (fork)
- ?????? - ??????
- PROFIT!!!1! - PROFIT!!!1!
## Examples
See [the examples](https://godoc.org/github.com/yunginnanet/girc-atomic#example-package--Bare)
within the documentation for real-world usecases. Here are a few real-world
usecases/examples/projects which utilize the real 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!
## Contributing ## Contributing
~~Please review the [CONTRIBUTING](CONTRIBUTING.md) doc for submitting issues/a guide ~~Please review the [CONTRIBUTING](CONTRIBUTING.md) doc for submitting issues/a guide
@ -89,7 +75,7 @@ on submitting pull requests and helping out.~~
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
later defiled by [some idiot](https://github.com/yunginnanet). ...and then later defiled by [some idiot](https://github.com/yunginnanet).
## References ## References

@ -18,9 +18,6 @@ import (
func (c *Client) registerBuiltins() { func (c *Client) registerBuiltins() {
c.debug.Print("registering built-in handlers") c.debug.Print("registering built-in handlers")
c.Handlers.mu.Lock()
defer c.Handlers.mu.Unlock()
// Built-in things that should always be supported. // Built-in things that should always be supported.
c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect)) c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect))
c.Handlers.register(true, false, PING, HandlerFunc(handlePING)) c.Handlers.register(true, false, PING, HandlerFunc(handlePING))
@ -129,6 +126,8 @@ func handleConnect(c *Client, e Event) {
// nickCollisionHandler helps prevent the client from having conflicting // nickCollisionHandler helps prevent the client from having conflicting
// nicknames with another bot, user, etc. // nicknames with another bot, user, etc.
//
//goland:noinspection GoUnusedParameter
func nickCollisionHandler(c *Client, e Event) { func nickCollisionHandler(c *Client, e Event) {
if c.Config.HandleNickCollide == nil { if c.Config.HandleNickCollide == nil {
c.Cmd.Nick(c.GetNick() + "_") c.Cmd.Nick(c.GetNick() + "_")
@ -146,6 +145,7 @@ func handlePING(c *Client, e Event) {
c.Cmd.Pong(e.Last()) c.Cmd.Pong(e.Last())
} }
//goland:noinspection GoUnusedParameter
func handlePONG(c *Client, e Event) { func handlePONG(c *Client, e Event) {
c.conn.lastPong.Store(time.Now()) c.conn.lastPong.Store(time.Now())
} }
@ -177,7 +177,7 @@ func handleJOIN(c *Client, e Event) {
defer c.state.notify(c, UPDATE_STATE) defer c.state.notify(c, UPDATE_STATE)
channel.addUser(user.Nick, user) channel.addUser(user.Nick.Load().(string), user)
user.addChannel(channel.Name, channel) user.addChannel(channel.Name, channel)
// Assume extended-join (ircv3). // Assume extended-join (ircv3).
@ -216,9 +216,6 @@ func handlePART(c *Client, e Event) {
return return
} }
c.state.Lock()
defer c.state.Unlock()
c.debug.Println("handlePart") c.debug.Println("handlePart")
defer c.debug.Println("handlePart done for " + e.Params[0]) defer c.debug.Println("handlePart done for " + e.Params[0])
@ -235,9 +232,7 @@ func handlePART(c *Client, e Event) {
if chn := c.LookupChannel(channel); chn != nil { if chn := c.LookupChannel(channel); chn != nil {
chn.UserList.Remove(e.Source.ID()) chn.UserList.Remove(e.Source.ID())
c.state.Unlock()
c.debug.Println(fmt.Sprintf("removed: %s, new count: %d", e.Source.ID(), chn.Len())) c.debug.Println(fmt.Sprintf("removed: %s, new count: %d", e.Source.ID(), chn.Len()))
c.state.Lock()
} else { } else {
c.debug.Println("failed to lookup channel: " + channel) c.debug.Println("failed to lookup channel: " + channel)
} }
@ -248,7 +243,6 @@ func handlePART(c *Client, e Event) {
} }
c.state.deleteUser(channel, e.Source.ID()) c.state.deleteUser(channel, e.Source.ID())
} }
// handleCREATIONTIME handles incoming TOPIC events and keeps channel tracking info // handleCREATIONTIME handles incoming TOPIC events and keeps channel tracking info
@ -351,8 +345,17 @@ func handleWHO(c *Client, e Event) {
} }
user.Host = host user.Host = host
user.Ident = ident user.Ident.Store(ident)
user.Mask = user.Nick + "!" + user.Ident + "@" + user.Host
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 user.Extras.Name = realname
if account != "0" { if account != "0" {
@ -553,7 +556,7 @@ func handleMOTD(c *Client, e Event) {
} }
// Otherwise, assume we're getting sent the MOTD line-by-line. // 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 += "\n"
} }
c.state.motd += e.Last() c.state.motd += e.Last()

17
cap.go

@ -62,7 +62,7 @@ func possibleCapList(c *Client) map[string][]string {
if !c.Config.DisableSTS && !c.Config.SSL { if !c.Config.DisableSTS && !c.Config.SSL {
// If fallback supported, and we failed recently, don't try negotiating STS. // 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 // 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 { 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") c.debug.Println("skipping strict transport policy negotiation; failed within the last 5 minutes")
} else { } else {
@ -122,8 +122,7 @@ func handleCAP(c *Client, e Event) {
if len(e.Params) >= 2 && e.Params[1] == CAP_DEL { if len(e.Params) >= 2 && e.Params[1] == CAP_DEL {
caps := parseCap(e.Last()) caps := parseCap(e.Last())
for capab := range caps { for capab := range caps {
// TODO: test the deletion. c.state.enabledCap.Remove(capab)
delete(c.state.enabledCap, capab)
} }
return return
} }
@ -194,10 +193,10 @@ func handleCAP(c *Client, e Event) {
enabled := strings.Split(e.Last(), " ") enabled := strings.Split(e.Last(), " ")
for _, capab := range enabled { for _, capab := range enabled {
if val, ok := c.state.tmpCap[capab]; ok { if val, ok := c.state.tmpCap[capab]; ok {
c.state.enabledCap[capab] = val c.state.enabledCap.Set(capab, val)
} else { continue
c.state.enabledCap[capab] = nil
} }
c.state.enabledCap.Remove(capab)
} }
// Anything client side that needs to be setup post-capability-acknowledgement, // Anything client side that needs to be setup post-capability-acknowledgement,
@ -205,7 +204,7 @@ func handleCAP(c *Client, e Event) {
// Handle STS, and only if it's something specifically we enabled (client // Handle STS, and only if it's something specifically we enabled (client
// may choose to disable girc automatic STS, and do it themselves). // 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 var isError bool
// Some things are updated in the policy depending on if the current // Some things are updated in the policy depending on if the current
// connection is over tls or not. // connection is over tls or not.
@ -285,7 +284,7 @@ func handleCAP(c *Client, e Event) {
// due to cap-notify, we can re-evaluate what we can support. // due to cap-notify, we can re-evaluate what we can support.
c.state.tmpCap = make(map[string]map[string]string) 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()}}) c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
// Don't "CAP END", since we want to authenticate. // Don't "CAP END", since we want to authenticate.
return return
@ -308,7 +307,7 @@ func handleCHGHOST(c *Client, e Event) {
user := c.state.lookupUser(e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil { if user != nil {
user.Ident = e.Params[0] user.Ident.Store(e.Params[0])
user.Host = e.Params[1] user.Host = e.Params[1]
} }

@ -51,9 +51,12 @@ type Tags map[string]string
// ParseTags parses out the key-value map of tags. raw should only be the tag // ParseTags parses out the key-value map of tags. raw should only be the tag
// data, not a full message. For example: // data, not a full message. For example:
// @aaa=bbb;ccc;example.com/ddd=eee //
// @aaa=bbb;ccc;example.com/ddd=eee
//
// NOT: // 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 // Technically, there is a length limit of 4096, but the server should reject
// tag messages longer than this. // tag messages longer than this.
@ -249,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 // Set escapes given value and saves it as the value for given key. Note that
// this is not concurrent safe. // this is not concurrent safe.
func (t Tags) Set(key, value string) error { func (t Tags) Set(key, value string) error {
if t == nil {
t = make(Tags)
}
if !validTag(key) { if !validTag(key) {
return fmt.Errorf("tag key %q is invalid", key) return fmt.Errorf("tag key %q is invalid", key)
} }

@ -34,26 +34,36 @@ func TestCapSupported(t *testing.T) {
} }
} }
func TestParseCap(t *testing.T) { var testsParseCap = []struct {
tests := []struct { in string
in string want map[string]map[string]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: "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", 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: "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: "example/name=test", want: map[string]map[string]string{"example/name": {"test": ""}}}, {
{ in: "userhost-in-names example/name example/name2=test=1,test2=true",
in: "userhost-in-names example/name example/name2=test=1,test2=true", want: map[string]map[string]string{
want: map[string]map[string]string{ "userhost-in-names": nil,
"userhost-in-names": nil, "example/name": nil,
"example/name": nil, "example/name2": {"test": "1", "test2": "true"},
"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) got := parseCap(tt.in)
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {

@ -11,7 +11,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"os" "os"
@ -22,6 +21,8 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
cmap "github.com/orcaman/concurrent-map/v2"
) )
// Client contains all of the information necessary to run a single IRC // Client contains all of the information necessary to run a single IRC
@ -223,13 +224,13 @@ type Config struct {
// server. // server.
// //
// Client expectations: // Client expectations:
// - Perform any proxy resolution. // - Perform any proxy resolution.
// - Check the reverse DNS and forward DNS match. // - Check the reverse DNS and forward DNS match.
// - Check the IP against suitable access controls (ipaccess, dnsbl, etc). // - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
// //
// More information: // More information:
// - https://ircv3.net/specs/extensions/webirc.html // - https://ircv3.net/specs/extensions/webirc.html
// - https://kiwiirc.com/docs/webirc // - https://kiwiirc.com/docs/webirc
type WebIRC struct { type WebIRC struct {
// Password that authenticates the WEBIRC command from this client. // Password that authenticates the WEBIRC command from this client.
Password string Password string
@ -275,10 +276,10 @@ func (conf *Config) isValid() error {
} }
if !IsValidNick(conf.Nick) { if !IsValidNick(conf.Nick) {
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified: " + conf.Nick)} return &ErrInvalidConfig{Conf: *conf, err: fmt.Errorf("bad nickname specified: %s", conf.Nick)}
} }
if !IsValidUser(conf.User) { if !IsValidUser(conf.User) {
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified: " + conf.User)} return &ErrInvalidConfig{Conf: *conf, err: fmt.Errorf("bad user/ident specified: %s", conf.Nick)}
} }
return nil return nil
@ -320,7 +321,7 @@ func New(config Config) *Client {
if envDebug { if envDebug {
c.debug = log.New(os.Stderr, "debug:", log.Ltime|log.Lshortfile) c.debug = log.New(os.Stderr, "debug:", log.Ltime|log.Lshortfile)
} else { } else {
c.debug = log.New(ioutil.Discard, "", 0) c.debug = log.New(io.Discard, "", 0)
} }
} else { } else {
if envDebug { if envDebug {
@ -341,7 +342,12 @@ func New(config Config) *Client {
c.Handlers = newCaller(c, c.debug) c.Handlers = newCaller(c, c.debug)
// Give ourselves a new state. // 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.RWMutex = &sync.RWMutex{}
c.state.reset(true) c.state.reset(true)
@ -601,7 +607,7 @@ func (c *Client) ChannelList() []string {
channels := make([]string, 0, len(c.state.channels.Keys())) channels := make([]string, 0, len(c.state.channels.Keys()))
for channel := range c.state.channels.IterBuffered() { for channel := range c.state.channels.IterBuffered() {
chn := channel.Val.(*Channel) chn := channel.Val
if !chn.UserIn(c.GetNick()) { if !chn.UserIn(c.GetNick()) {
continue continue
} }
@ -617,9 +623,9 @@ func (c *Client) ChannelList() []string {
func (c *Client) Channels() []*Channel { func (c *Client) Channels() []*Channel {
c.panicIfNotTracking() c.panicIfNotTracking()
channels := make([]*Channel, 0, len(c.state.channels)) channels := make([]*Channel, 0, c.state.channels.Count())
for channel := range c.state.channels.IterBuffered() { for channel := range c.state.channels.IterBuffered() {
chn := channel.Val.(*Channel) chn := channel.Val
channels = append(channels, chn.Copy()) channels = append(channels, chn.Copy())
} }
@ -634,13 +640,13 @@ func (c *Client) Channels() []*Channel {
func (c *Client) UserList() []string { func (c *Client) UserList() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
users := make([]string, 0, len(c.state.users)) users := make([]string, 0, c.state.users.Count())
for user := range c.state.users.IterBuffered() { for user := range c.state.users.IterBuffered() {
usr := user.Val.(*User) usr := user.Val
if usr.Stale { if usr.Stale {
continue continue
} }
users = append(users, usr.Nick) users = append(users, usr.Nick.Load().(string))
} }
sort.Strings(users) sort.Strings(users)
@ -652,14 +658,14 @@ func (c *Client) UserList() []string {
func (c *Client) Users() []*User { func (c *Client) Users() []*User {
c.panicIfNotTracking() c.panicIfNotTracking()
users := make([]*User, 0, len(c.state.users)) users := make([]*User, 0, c.state.users.Count())
for user := range c.state.users.IterBuffered() { for user := range c.state.users.IterBuffered() {
usr := user.Val.(*User) usr := user.Val
users = append(users, usr.Copy()) users = append(users, usr.Copy())
} }
sort.Slice(users, func(i, j int) bool { 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 return users
} }
@ -703,18 +709,15 @@ func (c *Client) IsInChannel(channel string) (in bool) {
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL). // during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
// Will panic if used when tracking has been disabled. Examples of usage: // Will panic if used when tracking has been disabled. Examples of usage:
// //
// nickLen, success := GetServerOpt("MAXNICKLEN") // nickLen, success := GetServerOpt("MAXNICKLEN")
//
func (c *Client) GetServerOpt(key string) (result string, ok bool) { func (c *Client) GetServerOpt(key string) (result string, ok bool) {
c.panicIfNotTracking() c.panicIfNotTracking()
oi, ok := c.state.serverOptions.Get(key) result, ok = c.state.serverOptions.Get(key)
if !ok { if !ok {
return "", ok return "", ok
} }
result = oi.(string)
if len(result) > 0 { if len(result) > 0 {
ok = true ok = true
} }
@ -727,7 +730,7 @@ func (c *Client) GetServerOpt(key string) (result string, ok bool) {
func (c *Client) GetServerOptions() []byte { func (c *Client) GetServerOptions() []byte {
o := make(map[string]string) o := make(map[string]string)
for opt := range c.state.serverOptions.IterBuffered() { for opt := range c.state.serverOptions.IterBuffered() {
o[opt.Key] = opt.Val.(string) o[opt.Key] = opt.Val
} }
jcytes, _ := json.Marshal(o) jcytes, _ := json.Marshal(o)
return jcytes return jcytes
@ -800,15 +803,13 @@ func (c *Client) HasCapability(name string) (has bool) {
name = strings.ToLower(name) name = strings.ToLower(name)
c.state.RLock() for capab := range c.state.enabledCap.IterBuffered() {
for key := range c.state.enabledCap { key := strings.ToLower(capab.Key)
key = strings.ToLower(key)
if key == name { if key == name {
has = true has = true
break break
} }
} }
c.state.RUnlock()
return has return has
} }
@ -850,7 +851,7 @@ func (c *Client) debugLogEvent(e *Event, dropped bool) {
if pretty, ok := e.Pretty(); ok { if pretty, ok := e.Pretty(); ok {
fmt.Fprintln(c.Config.Out, StripRaw(pretty)) _, _ = fmt.Fprintln(c.Config.Out, StripRaw(pretty))
} }
} }
} }

@ -93,14 +93,24 @@ func TestClientLifetime(t *testing.T) {
func TestClientUptime(t *testing.T) { func TestClientUptime(t *testing.T) {
c, conn, server := genMockConn() c, conn, server := genMockConn()
defer conn.Close() defer func() {
defer server.Close() 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) go mockReadBuffer(conn)
done := make(chan struct{}, 1) done := make(chan struct{}, 1)
c.Handlers.Add(INITIALIZED, func(c *Client, e Event) { close(done) }) 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() defer c.Close()
select { select {
@ -138,14 +148,24 @@ func TestClientUptime(t *testing.T) {
func TestClientGet(t *testing.T) { func TestClientGet(t *testing.T) {
c, conn, server := genMockConn() c, conn, server := genMockConn()
defer conn.Close() defer func() {
defer server.Close() 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) go mockReadBuffer(conn)
done := make(chan struct{}, 1) done := make(chan struct{}, 1)
c.Handlers.Add(INITIALIZED, func(c *Client, e Event) { close(done) }) 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() defer c.Close()
select { select {
@ -169,8 +189,14 @@ func TestClientGet(t *testing.T) {
func TestClientClose(t *testing.T) { func TestClientClose(t *testing.T) {
c, conn, server := genMockConn() c, conn, server := genMockConn()
defer server.Close() defer func() {
defer conn.Close() 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) go mockReadBuffer(conn)
errchan := make(chan error, 1) errchan := make(chan error, 1)

@ -4,6 +4,7 @@ package girc
IRCNumToStr takes in a numeric IRC code and returns the relevant girc name. 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. IRCNumToStr accepts a string because that's how we tend to receive the codes.
*/ */
//goland:noinspection GoUnusedExportedFunction
func IRCNumToStr(code string) string { func IRCNumToStr(code string) string {
if _, ok := noTranslate[code]; ok { if _, ok := noTranslate[code]; ok {
return code return code

@ -36,7 +36,7 @@ func (cmd *Commands) Join(channels ...string) {
continue continue
} }
if len(buffer) == 0 { if buffer == "" {
buffer = channels[i] buffer = channels[i]
} else { } else {
buffer += "," + channels[i] 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 // Messagef sends a formated PRIVMSG to target (either channel, service, or
// user). // user).
func (cmd *Commands) Messagef(target, format string, a ...interface{}) { func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
cmd.Message(target, fmt.Sprintf(Fmt(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 // ErrInvalidSource is returned when a method needs to know the origin of an
@ -119,79 +120,83 @@ func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
// server.) // server.)
var ErrInvalidSource = errors.New("event has nil or invalid source address") 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 // 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 // originated from. See also ReplyTo(). Panics if the incoming event has no
// source. // source.
func (cmd *Commands) Reply(event Event, message string) { func (cmd *Commands) Reply(event Event, message string) error {
if event.Source == nil { if event.Source == nil {
panic(ErrInvalidSource) return ErrInvalidSource
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
cmd.Message(event.Params[0], message) cmd.Message(event.Params[0], message)
return return nil
} }
cmd.Message(event.Source.Name, message) cmd.Message(event.Source.Name, message)
return nil
} }
// ReplyKick kicks the source of the event from the channel where the event originated // ReplyKick kicks the source of the event from the channel where the event originated
func (cmd *Commands) ReplyKick(event Event, reason string) { func (cmd *Commands) ReplyKick(event Event, reason string) error {
if event.Source == nil { if event.Source == nil {
panic(ErrInvalidSource) return ErrInvalidSource
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
cmd.Kick(event.Params[0], event.Source.Name, reason) 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. // 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. // Additionally, if a reason is provided, it will send a message to the channel.
func (cmd *Commands) ReplyBan(event Event, reason string) { func (cmd *Commands) ReplyBan(event Event, reason string) (err error) {
if event.Source == nil { if event.Source == nil {
panic(ErrInvalidSource) return ErrInvalidSource
} }
if reason != "" { if reason != "" {
cmd.Replyf(event, "{red}{b}[BAN] {r}%s", reason) err = cmd.Replyf(event, "{red}{b}[BAN] {r}%s", reason)
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
cmd.Ban(event.Params[0], event.Source.Name) 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 // 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 // where the supplied event originated from. See also ReplyTof(). Panics if
// the incoming event has no source. // the incoming event has no source.
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) { // Formatted means both in the sense of Sprintf as well as girc style macros.
cmd.Reply(event, fmt.Sprintf(Fmt(format), a...)) 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 // ReplyTo sends a reply to a channel or user, based on where the supplied
// event originated from. ReplyTo(), when originating from a channel will // event originated from. ReplyTo(), when originating from a channel will
// default to replying with "<user>, <message>". See also Reply(). Panics if // default to replying with "<user>, <message>". See also Reply(). Panics if
// the incoming event has no source. // 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 { if event.Source == nil {
panic(ErrInvalidSource) return ErrInvalidSource
} }
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
cmd.Message(event.Params[0], event.Source.Name+", "+message) cmd.Message(event.Params[0], event.Source.Name+", "+message)
return } else {
cmd.Message(event.Source.Name, message)
} }
return nil
cmd.Message(event.Source.Name, message)
} }
// ReplyTof sends a reply to a channel or user with a format string, based // ReplyTof sends a reply to a channel or user with a format string, based
// on where the supplied event originated from. ReplyTo(), when originating // on where the supplied event originated from. ReplyTo(), when originating
// from a channel will default to replying with "<user>, <message>". See // from a channel will default to replying with "<user>, <message>". See
// also Replyf(). Panics if the incoming event has no source. // also Replyf(). Panics if the incoming event has no source.
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) { // Formatted means both in the sense of Sprintf as well as girc style macros.
cmd.ReplyTo(event, fmt.Sprintf(Fmt(format), a...)) 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, // Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
@ -215,9 +220,9 @@ func (cmd *Commands) Notice(target, message string) {
} }
// Noticef sends a formated NOTICE to target (either channel, service, or // 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{}) { 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 // SendRaw sends a raw string (or multiple) to the server, without carriage
@ -239,14 +244,21 @@ func (cmd *Commands) SendRaw(raw ...string) error {
} }
// SendRawf sends a formated string back to the server, without carriage // 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 { 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 // Topic sets the topic of channel to message. Does not verify the length
// of the topic. // of the topic.
func (cmd *Commands) Topic(channel, message string) { 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)}}) cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, Fmt(message)}})
} }
@ -286,6 +298,22 @@ func (cmd *Commands) Oper(user, pass string) {
cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true}) 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 // 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 // channel, with reason. If reason is blank, one will not be sent to the
// server. // server.
@ -293,7 +321,6 @@ func (cmd *Commands) Kick(channel, user, reason string) {
if reason != "" { if reason != "" {
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user, reason}}) cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user, reason}})
} }
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}}) cmd.c.Send(&Event{Command: KICK, Params: []string{channel, user}})
} }
@ -366,7 +393,7 @@ func (cmd *Commands) List(channels ...string) {
continue continue
} }
if len(buffer) == 0 { if buffer == "" {
buffer = channels[i] buffer = channels[i]
} else { } else {
buffer += "," + channels[i] buffer += "," + channels[i]

35
conn.go

@ -12,6 +12,8 @@ import (
"net" "net"
"sync/atomic" "sync/atomic"
"time" "time"
"git.tcp.direct/kayos/common/pool"
) )
// Messages are delimited with CR and LF line endings, we're using the last // Messages are delimited with CR and LF line endings, we're using the last
@ -55,6 +57,8 @@ type Dialer interface {
Dial(network, address string) (net.Conn, error) Dial(network, address string) (net.Conn, error)
} }
var strs = pool.NewStringFactory()
// newConn sets up and returns a new connection to the server. // newConn sets up and returns a new connection to the server.
func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ircConn, error) { func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ircConn, error) {
if err := conf.isValid(); err != nil { if err := conf.isValid(); err != nil {
@ -69,7 +73,11 @@ func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ir
if conf.Bind != "" { if conf.Bind != "" {
var local *net.TCPAddr 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 { if err != nil {
return nil, err return nil, err
} }
@ -167,18 +175,6 @@ func (c *ircConn) decode() (event *Event, err error) {
return event, nil 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() { func (c *ircConn) newReadWriter() {
c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock)) c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock))
} }
@ -513,15 +509,13 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, working *int32)
// Check if tags exist on the event. If they do, and message-tags // Check if tags exist on the event. If they do, and message-tags
// isn't a supported capability, remove them from the event. // isn't a supported capability, remove them from the event.
if event.Tags != nil { if event.Tags != nil {
c.state.RLock()
var in bool var in bool
for i := 0; i < len(c.state.enabledCap); i++ { for i := 0; i < c.state.enabledCap.Count(); i++ {
if _, ok := c.state.enabledCap["message-tags"]; ok { if _, ok := c.state.enabledCap.Get("message-tags"); ok {
in = true in = true
break break
} }
} }
c.state.RUnlock()
if !in { if !in {
event.Tags = Tags{} event.Tags = Tags{}
@ -595,6 +589,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, working *int32)
started := time.Now() started := time.Now()
past := false past := false
pingSent := false
for { for {
select { select {
@ -609,9 +604,8 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, working *int32)
past = true past = true
} }
if time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(120*time.Second) { if pingSent && time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(180*time.Second) {
// It's 60 seconds over what out ping delay is, connection // It's 180 seconds over what out ping delay is, connection has probably dropped.
// has probably dropped.
err := ErrTimedOut{ err := ErrTimedOut{
TimeSinceSuccess: time.Since(c.conn.lastPong.Load().(time.Time)), TimeSinceSuccess: time.Since(c.conn.lastPong.Load().(time.Time)),
@ -629,6 +623,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, working *int32)
c.conn.lastPing.Store(time.Now()) c.conn.lastPing.Store(time.Now())
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
pingSent = true
case <-ctx.Done(): case <-ctx.Done():
return return
} }

@ -14,7 +14,7 @@ import (
"time" "time"
) )
func mockBuffers() (in *bytes.Buffer, out *bytes.Buffer, irc *ircConn) { func mockBuffers() (in, out *bytes.Buffer, irc *ircConn) {
in = &bytes.Buffer{} in = &bytes.Buffer{}
out = &bytes.Buffer{} out = &bytes.Buffer{}
irc = &ircConn{ irc = &ircConn{
@ -88,7 +88,7 @@ func TestRate(t *testing.T) {
} }
} }
func genMockConn() (client *Client, clientConn net.Conn, serverConn net.Conn) { func genMockConn() (client *Client, clientConn, serverConn net.Conn) {
client = New(Config{ client = New(Config{
Server: "dummy.int", Server: "dummy.int",
Port: 6667, Port: 6667,
@ -107,7 +107,7 @@ func mockReadBuffer(conn net.Conn) {
// Accept all outgoing writes from the client. // Accept all outgoing writes from the client.
b := bufio.NewReader(conn) b := bufio.NewReader(conn)
for { for {
conn.SetReadDeadline(time.Now().Add(10 * time.Second)) _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
_, err := b.ReadString(byte('\n')) _, err := b.ReadString(byte('\n'))
if err != nil { if err != nil {
return return

@ -5,6 +5,8 @@
package girc package girc
// Standard CTCP based constants. // Standard CTCP based constants.
//
//goland:noinspection ALL
const ( const (
CTCP_ACTION = "ACTION" CTCP_ACTION = "ACTION"
CTCP_PING = "PING" CTCP_PING = "PING"
@ -20,6 +22,8 @@ const (
// Emulated event commands used to allow easier hooks into the changing // Emulated event commands used to allow easier hooks into the changing
// state of the client. // state of the client.
//
//goland:noinspection ALL
const ( const (
UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated. 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. UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated.
@ -33,6 +37,8 @@ const (
) )
// User/channel prefixes :: RFC1459. // User/channel prefixes :: RFC1459.
//
//goland:noinspection ALL
const ( const (
DefaultPrefixes = "(ov)@+" // the most common default prefixes DefaultPrefixes = "(ov)@+" // the most common default prefixes
ModeAddPrefix = "+" // modes are being added ModeAddPrefix = "+" // modes are being added
@ -48,6 +54,8 @@ const (
) )
// User modes :: RFC1459; section 4.2.3.2. // User modes :: RFC1459; section 4.2.3.2.
//
//goland:noinspection ALL
const ( const (
UserModeInvisible = "i" // invisible UserModeInvisible = "i" // invisible
UserModeOperator = "o" // server operator UserModeOperator = "o" // server operator
@ -56,6 +64,8 @@ const (
) )
// Channel modes :: RFC1459; section 4.2.3.1. // Channel modes :: RFC1459; section 4.2.3.1.
//
//goland:noinspection ALL
const ( const (
ModeDefaults = "beI,k,l,imnpst" // the most common default modes ModeDefaults = "beI,k,l,imnpst" // the most common default modes
@ -75,6 +85,8 @@ const (
) )
// IRC commands :: RFC2812; section 3 :: RFC2813; section 4. // IRC commands :: RFC2812; section 3 :: RFC2813; section 4.
//
//goland:noinspection ALL
const ( const (
ADMIN = "ADMIN" ADMIN = "ADMIN"
AWAY = "AWAY" AWAY = "AWAY"
@ -127,6 +139,8 @@ const (
) )
// Numeric IRC reply mapping :: RFC2812; section 5. // Numeric IRC reply mapping :: RFC2812; section 5.
//
//goland:noinspection ALL
const ( const (
RPL_WELCOME = "001" RPL_WELCOME = "001"
RPL_YOURHOST = "002" RPL_YOURHOST = "002"
@ -270,6 +284,8 @@ const (
) )
// IRCv3 commands and extensions :: http://ircv3.net/irc/. // IRCv3 commands and extensions :: http://ircv3.net/irc/.
//
//goland:noinspection ALL
const ( const (
AUTHENTICATE = "AUTHENTICATE" AUTHENTICATE = "AUTHENTICATE"
MONITOR = "MONITOR" MONITOR = "MONITOR"
@ -293,6 +309,8 @@ const (
) )
// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/. // Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/.
//
//goland:noinspection ALL
const ( const (
RPL_LOGGEDIN = "900" RPL_LOGGEDIN = "900"
RPL_LOGGEDOUT = "901" RPL_LOGGEDOUT = "901"
@ -313,6 +331,8 @@ const (
) )
// Numeric IRC event mapping :: RFC2812; section 5.3. // Numeric IRC event mapping :: RFC2812; section 5.3.
//
//goland:noinspection ALL
const ( const (
RPL_STATSCLINE = "213" RPL_STATSCLINE = "213"
RPL_STATSNLINE = "214" RPL_STATSNLINE = "214"
@ -341,6 +361,8 @@ const (
) )
// Misc. // Misc.
//
//goland:noinspection ALL
const ( const (
ERR_TOOMANYMATCHES = "416" // IRCNet. ERR_TOOMANYMATCHES = "416" // IRCNet.
RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode. RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode.
@ -351,6 +373,8 @@ const (
) )
// As seen in the wild. // As seen in the wild.
//
//goland:noinspection ALL
const ( const (
RPL_WHOISAUTHNAME = "330" RPL_WHOISAUTHNAME = "330"
RPL_WHOISTLS = "671" RPL_WHOISTLS = "671"

54
ctcp.go

@ -5,11 +5,12 @@
package girc package girc
import ( import (
"fmt"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"time" "time"
cmap "github.com/orcaman/concurrent-map/v2"
) )
// ctcpDelim if the delimiter used for CTCP formatted events/messages. // 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 // EncodeCTCPRaw is much like EncodeCTCP, however accepts a raw command and
// string as input. // string as input.
func EncodeCTCPRaw(cmd, text string) (out string) { func EncodeCTCPRaw(cmd, text string) (out string) {
if len(cmd) <= 0 { if cmd == "" {
return "" return cmd
} }
out = string(ctcpDelim) + cmd out = string(ctcpDelim) + cmd
@ -123,12 +124,12 @@ type CTCP struct {
// mu is the mutex that should be used when accessing any ctcp handlers. // mu is the mutex that should be used when accessing any ctcp handlers.
mu sync.RWMutex mu sync.RWMutex
// handlers is a map of CTCP message -> functions. // handlers is a map of CTCP message -> functions.
handlers map[string]CTCPHandler handlers cmap.ConcurrentMap[string, CTCPHandler]
} }
// newCTCP returns a new clean CTCP handler. // newCTCP returns a new clean CTCP handler.
func newCTCP() *CTCP { 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 // call executes the necessary CTCP handler for the incoming event/CTCP
@ -138,27 +139,16 @@ func (c *CTCP) call(client *Client, event *CTCPEvent) {
if client.Config.RecoverFunc != nil && event.Origin != nil { if client.Config.RecoverFunc != nil && event.Origin != nil {
defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3) defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3)
} }
// Support wildcard CTCP event handling. Gets executed first before // Support wildcard CTCP event handling. Gets executed first before
// regular event handlers. // regular event handlers.
if _, ok := c.handlers["*"]; ok { if val, ok := c.handlers.Get("*"); ok && val != nil {
c.handlers["*"](client, *event) val(client, *event)
} }
val, ok := c.handlers.Get(event.Command)
if _, ok := c.handlers[event.Command]; !ok { if !ok || val == nil || event.Command == CTCP_ACTION {
// If ACTION, don't do anything.
if event.Command == CTCP_ACTION {
return
}
// Send a ERRMSG reply, if we know who sent it.
if event.Source != nil && IsValidNick(event.Source.ID()) {
client.Cmd.SendCTCPReply(event.Source.ID(), CTCP_ERRMSG, "that is an unknown CTCP query")
}
return return
} }
val(client, *event)
c.handlers[event.Command](client, *event)
} }
// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty // parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty
@ -190,10 +180,7 @@ func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
if cmd = c.parseCMD(cmd); cmd == "" { if cmd = c.parseCMD(cmd); cmd == "" {
return return
} }
c.mu.Lock() c.handlers.Set(cmd, handler)
defer c.mu.Unlock()
c.handlers[cmd] = handler
} }
// SetBg is much like Set, however the handler is executed in the background, // SetBg is much like Set, however the handler is executed in the background,
@ -210,18 +197,12 @@ func (c *CTCP) Clear(cmd string) {
if cmd = c.parseCMD(cmd); cmd == "" { if cmd = c.parseCMD(cmd); cmd == "" {
return return
} }
c.handlers.Remove(cmd)
c.mu.Lock()
delete(c.handlers, cmd)
c.mu.Unlock()
} }
// ClearAll removes all currently setup and re-sets the default handlers. // ClearAll removes all currently setup and re-sets the default handlers.
func (c *CTCP) ClearAll() { func (c *CTCP) ClearAll() {
c.mu.Lock() c.handlers = cmap.New[CTCPHandler]()
c.handlers = map[string]CTCPHandler{}
c.mu.Unlock()
// Register necessary handlers. // Register necessary handlers.
c.addDefaultHandlers() c.addDefaultHandlers()
} }
@ -311,8 +292,7 @@ func handleCTCPFinger(client *Client, ctcp CTCPEvent) {
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, client.Config.Finger) client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, client.Config.Finger)
return 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) // 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))) // client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
} }

@ -9,26 +9,40 @@ import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "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) { func TestEncodeCTCP(t *testing.T) {
type args struct { for _, tt := range testsEncodeCTCP {
ctcp *CTCPEvent if got := EncodeCTCP(tt.test); got != tt.want {
}
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 {
t.Errorf("%s: encodeCTCP() = %q, want %q", tt.name, 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) 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") t.Fatal("regular execution: call() didn't increase counter")
} }
ctcp.Clear("TEST") ctcp.Clear("TEST")
@ -120,7 +135,8 @@ func TestCall(t *testing.T) {
}) })
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}) 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") t.Fatal("goroutine execution: call() in goroutine didn't increase counter")
} }
ctcp.Clear("TEST") ctcp.Clear("TEST")
@ -129,14 +145,15 @@ func TestCall(t *testing.T) {
atomic.AddUint64(&counter, 1) 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") t.Fatal("wildcard execution: call() didn't increase counter")
} }
ctcp.Clear("*") ctcp.Clear("*")
ctcp.Clear("TEST") 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") t.Fatal("empty execution: call() with no handler incremented the counter")
} }
} }
@ -145,13 +162,13 @@ func TestSet(t *testing.T) {
ctcp := newCTCP() ctcp := newCTCP()
ctcp.Set("TEST-1", func(client *Client, event CTCPEvent) {}) 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") t.Fatal("Set('TEST') allowed invalid command")
} }
ctcp.Set("TEST", func(client *Client, event CTCPEvent) {}) ctcp.Set("TEST", func(client *Client, event CTCPEvent) {})
// Make sure it's there. // 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") 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.Set("TEST", func(client *Client, event CTCPEvent) {})
ctcp.Clear("TEST") 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") 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.Set("TEST2", func(client *Client, event CTCPEvent) {})
ctcp.ClearAll() ctcp.ClearAll()
_, first := ctcp.handlers["TEST1"] _, first := ctcp.handlers.Get("TEST1")
_, second := ctcp.handlers["TEST2"] _, second := ctcp.handlers.Get("TEST2")
if first || second { if first || second {
t.Fatalf("ctcp.ClearAll() didn't remove all handlers: 1: %v 2: %v", first, second) t.Fatalf("ctcp.ClearAll() didn't remove all handlers: 1: %v 2: %v", first, second)

@ -52,7 +52,7 @@ func ParseEvent(raw string) (e *Event) {
i = 0 i = 0
} }
if raw[0] == messagePrefix { if raw != "" && raw[0] == messagePrefix {
// Prefix ends with a space. // Prefix ends with a space.
i = strings.IndexByte(raw, eventSpace) i = strings.IndexByte(raw, eventSpace)
@ -313,7 +313,9 @@ func (e *Event) Bytes() []byte {
buffer.Truncate(maxLength) 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. // Strip newlines and carriage returns.
for i := 0; i < len(out); i++ { for i := 0; i < len(out); i++ {
@ -638,7 +640,7 @@ func (s *Source) IsHostmask() bool {
// IsServer returns true if this source looks like a server name. // IsServer returns true if this source looks like a server name.
func (s *Source) IsServer() bool { 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 // writeTo is an utility function to write the source to the bytes.Buffer

@ -111,7 +111,7 @@ func TestParseEvent(t *testing.T) {
} }
if got == nil { 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 { if got.String() != tt.want {
@ -133,6 +133,7 @@ func TestParseEvent(t *testing.T) {
} }
} }
//goland:noinspection GoNilness
func TestEventCopy(t *testing.T) { func TestEventCopy(t *testing.T) {
var nilEvent *Event var nilEvent *Event

@ -59,7 +59,7 @@ func Example_simple() {
client.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) { client.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
if strings.Contains(e.Last(), "hello") { if strings.Contains(e.Last(), "hello") {
c.Cmd.ReplyTo(e, "hello world!") _ = c.Cmd.ReplyTo(e, "hello world!")
return return
} }
@ -99,7 +99,7 @@ func Example_commands() {
client.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) { client.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
if strings.HasPrefix(e.Last(), "!hello") { 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 return
} }

@ -66,7 +66,7 @@ var fmtCodes = map[string]string{
// //
// For example: // 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 { func Fmt(text string) string {
var last = -1 var last = -1
for i := 0; i < len(text); i++ { for i := 0; i < len(text); i++ {
@ -127,10 +127,10 @@ func Fmt(text string) string {
// See Fmt() for more information. // See Fmt() for more information.
func TrimFmt(text string) string { func TrimFmt(text string) string {
for color := range fmtColors { 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 { 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 return text
@ -138,7 +138,7 @@ func TrimFmt(text string) string {
// This is really the only fastest way of doing this (marginally better than // This is really the only fastest way of doing this (marginally better than
// actually trying to parse it manually.) // 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. // StripRaw tries to strip all ASCII format codes that are used for IRC.
// Primarily, foreground/background colors, and other control bytes like // Primarily, foreground/background colors, and other control bytes like
@ -148,7 +148,7 @@ func StripRaw(text string) string {
text = reStripColor.ReplaceAllString(text, "") text = reStripColor.ReplaceAllString(text, "")
for _, code := range fmtCodes { for _, code := range fmtCodes {
text = strings.Replace(text, code, "", -1) text = strings.ReplaceAll(text, code, "")
} }
return text return text
@ -164,12 +164,12 @@ func StripRaw(text string) string {
// all ASCII printable chars. This function will NOT do that for // all ASCII printable chars. This function will NOT do that for
// compatibility reasons. // compatibility reasons.
// //
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring // channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
// [ ":" chanstring ] // [ ":" chanstring ]
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B // chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
// chanstring = / 0x2D-0x39 / 0x3B-0xFF // chanstring = / 0x2D-0x39 / 0x3B-0xFF
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":" // ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 ) // channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
func IsValidChannel(channel string) bool { func IsValidChannel(channel string) bool {
if len(channel) <= 1 || len(channel) > 50 { if len(channel) <= 1 || len(channel) > 50 {
return false return false
@ -214,12 +214,12 @@ func IsValidChannel(channel string) bool {
// IsValidNick validates an IRC nickname. Note that this does not validate // IsValidNick validates an IRC nickname. Note that this does not validate
// IRC nickname length. // IRC nickname length.
// //
// nickname = ( letter / special ) *8( letter / digit / special / "-" ) // nickname = ( letter / special ) *8( letter / digit / special / "-" )
// letter = 0x41-0x5A / 0x61-0x7A // letter = 0x41-0x5A / 0x61-0x7A
// digit = 0x30-0x39 // digit = 0x30-0x39
// special = 0x5B-0x60 / 0x7B-0x7D // special = 0x5B-0x60 / 0x7B-0x7D
func IsValidNick(nick string) bool { func IsValidNick(nick string) bool {
if len(nick) <= 0 { if nick == "" {
return false 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. // not be supported on all networks. Some limit this to only a single period.
// //
// Per RFC: // 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 { func IsValidUser(name string) bool {
if len(name) <= 0 { if name == "" {
return false return false
} }
@ -324,7 +325,7 @@ func Glob(input, match string) bool {
if len(parts) == 1 { if len(parts) == 1 {
// No globs, test for equality. // No globs, test for equality.
return input == match return strings.EqualFold(input, match)
} }
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar) leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)

@ -7,6 +7,7 @@ package girc
import ( import (
"strings" "strings"
"testing" "testing"
"unicode/utf8"
) )
func BenchmarkFormat(b *testing.B) { func BenchmarkFormat(b *testing.B) {
@ -47,207 +48,294 @@ func BenchmarkStripRawLong(b *testing.B) {
} }
} }
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) { func TestFormat(t *testing.T) {
type args struct { for _, tt := range testsFormat {
text string if got := Fmt(tt.test); got != tt.want {
} t.Errorf("%s: Format(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
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)
} }
} }
} }
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) { func TestStripFormat(t *testing.T) {
type args struct { for _, tt := range testsStripFormat {
text string if got := TrimFmt(tt.test); got != tt.want {
} t.Errorf("%s: StripFormat(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
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)
} }
} }
} }
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) { func TestStripRaw(t *testing.T) {
type args struct { for _, tt := range testsStripRaw {
text string if got := StripRaw(Fmt(tt.test)); got != tt.want {
} t.Fatalf("%s: StripRaw(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
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)
} }
} }
} }
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) { func TestIsValidNick(t *testing.T) {
type args struct { for _, tt := range testsValidNick {
nick string if got := IsValidNick(tt.test); got != tt.want {
} t.Errorf("%s: IsValidNick(%q) = %v, want %v", tt.name, tt.test, got, tt.want)
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)
} }
} }
} }
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) { func TestIsValidChannel(t *testing.T) {
type args struct { for _, tt := range testsValidChannel {
channel string if got := IsValidChannel(tt.test); got != tt.want {
} t.Errorf("%s: IsValidChannel(%q) = %v, want %v", tt.name, tt.test, got, tt.want)
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)
} }
} }
} }
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) { func TestIsValidUser(t *testing.T) {
type args struct { for _, tt := range testsValidUser {
name string if got := IsValidUser(tt.test); got != tt.want {
} t.Errorf("%s: IsValidUser(%q) = %v, want %v", tt.name, tt.test, got, tt.want)
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)
} }
} }
} }
func TestToRFC1459(t *testing.T) { var testsToRFC1459 = []struct {
cases := []struct { in string
in string want string
want string }{
}{ {"", ""},
{"", ""}, {"a", "a"},
{"a", "a"}, {"abcd", "abcd"},
{"abcd", "abcd"}, {"AbcD", "abcd"},
{"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 { if got := ToRFC1459(tt.in); got != tt.want {
t.Errorf("ToRFC1459() = %q, want %q", got, tt.want) t.Errorf("ToRFC1459() = %q, want %q", got, tt.want)
} }
} }
} }
//func BenchmarkGlob(b *testing.B) { func BenchmarkGlob(b *testing.B) {
// for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
// if !Glob("*quick*fox*dog", "The quick brown fox jumped over the lazy dog") { if !Glob("*quick*fox*dog", "The quick brown fox jumped over the lazy dog") {
// b.Fatalf("should match") b.Fatalf("should match")
// } }
// } }
// }
// return
//}
func testGlobMatch(t *testing.T, subj, pattern string) { func testGlobMatch(t *testing.T, subj, pattern string) {
if !Glob(subj, pattern) { if !Glob(subj, pattern) {
@ -310,27 +398,37 @@ func TestPatternWithoutGlobs(t *testing.T) {
testGlobMatch(t, "test", "test") testGlobMatch(t, "test", "test")
} }
func TestGlob(t *testing.T) { var testsGlob = []string{
cases := []string{ "*test", // Leading.
"*test", // Leading. "this*", // Trailing.
"this*", // Trailing. "this*test", // Middle.
"this*test", // Middle. "*is *", // String in between two.
"*is *", // String in between two. "*is*a*", // Lots.
"*is*a*", // Lots. "**test**", // Double glob characters.
"**test**", // Double glob characters. "**is**a***test*", // Varying number.
"**is**a***test*", // Varying number. "* *", // White space between.
"* *", // White space between. "*", // Lone.
"*", // Lone. "**********", // Nothing but globs.
"**********", // Nothing but globs. "*Ѿ*", // Unicode.
"*Ѿ*", // Unicode. "*is a ϗѾ *", // Mixed ASCII/unicode.
"*is a ϗѾ *", // Mixed ASCII/unicode. }
func FuzzGlob(f *testing.F) {
for _, tc := range testsGlob {
f.Add(tc, tc)
} }
for _, pattern := range cases { f.Fuzz(func(t *testing.T, orig, orig2 string) {
_ = Glob(orig, orig2)
})
}
func TestGlob(t *testing.T) {
for _, pattern := range testsGlob {
testGlobMatch(t, "this is a ϗѾ test", pattern) testGlobMatch(t, "this is a ϗѾ test", pattern)
} }
cases = []string{ cases := []string{
"test*", // Implicit substring match. "test*", // Implicit substring match.
"*is", // Partial match. "*is", // Partial match.
"*no*", // Globs without a match between them. "*no*", // Globs without a match between them.

10
go.mod

@ -1,13 +1,9 @@
module github.com/yunginnanet/girc-atomic module github.com/yunginnanet/girc-atomic
go 1.17 go 1.20
require ( require (
git.tcp.direct/kayos/common v0.8.1
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/orcaman/concurrent-map v1.0.0 github.com/orcaman/concurrent-map/v2 v2.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

12
go.sum

@ -1,11 +1,12 @@
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 h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -14,6 +15,5 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -15,7 +15,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/orcaman/concurrent-map" cmap "github.com/orcaman/concurrent-map/v2"
) )
// RunHandlers manually runs handlers for a given event. // RunHandlers manually runs handlers for a given event.
@ -25,15 +25,18 @@ func (c *Client) RunHandlers(event *Event) {
return return
} }
s := strs.Get()
// Log the event. // Log the event.
prefix := "< " s.MustWriteString("< ")
if event.Echo { 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 c.Config.Out != nil {
if pretty, ok := event.Pretty(); ok { if pretty, ok := event.Pretty(); ok {
fmt.Fprintln(c.Config.Out, StripRaw(pretty)) _, _ = fmt.Fprintln(c.Config.Out, StripRaw(pretty))
} }
} }
@ -78,7 +81,7 @@ func (f HandlerFunc) Execute(client *Client, event Event) {
// //
// command and cuid are both strings. // command and cuid are both strings.
type nestedHandlers struct { type nestedHandlers struct {
cm cmap.ConcurrentMap cm cmap.ConcurrentMap[string, cmap.ConcurrentMap[string, Handler]]
} }
type handlerTuple struct { type handlerTuple struct {
@ -87,40 +90,37 @@ type handlerTuple struct {
} }
func newNestedHandlers() *nestedHandlers { func newNestedHandlers() *nestedHandlers {
return &nestedHandlers{cm: cmap.New()} return &nestedHandlers{cm: cmap.New[cmap.ConcurrentMap[string, Handler]]()}
} }
func (nest *nestedHandlers) len() (total int) { func (nest *nestedHandlers) len() (total int) {
for hs := range nest.cm.IterBuffered() { for hndlrs := range nest.cm.IterBuffered() {
hndlrs := hs.Val.(cmap.ConcurrentMap) total += len(hndlrs.Val.Keys())
total += len(hndlrs.Keys())
} }
return return
} }
func (nest *nestedHandlers) lenFor(cmd string) (total int) { func (nest *nestedHandlers) lenFor(cmd string) (total int) {
cmd = strings.ToUpper(cmd) cmd = strings.ToUpper(cmd)
hs, ok := nest.cm.Get(cmd) hndlrs, ok := nest.cm.Get(cmd)
if !ok { if !ok {
return 0 return 0
} }
hndlrs := hs.(cmap.ConcurrentMap) return hndlrs.Count()
return len(hndlrs.Keys())
} }
func (nest *nestedHandlers) getAllHandlersFor(s string) (handlers chan handlerTuple, ok bool) { func (nest *nestedHandlers) getAllHandlersFor(s string) (handlers chan handlerTuple, ok bool) {
var h interface{} var h cmap.ConcurrentMap[string, Handler]
h, ok = nest.cm.Get(s) h, ok = nest.cm.Get(s)
if !ok { if !ok {
return return
} }
hm := h.(cmap.ConcurrentMap)
handlers = make(chan handlerTuple) handlers = make(chan handlerTuple)
go func() { go func() {
for hi := range hm.IterBuffered() { for hi := range h.IterBuffered() {
ht := handlerTuple{ ht := handlerTuple{
hi.Key, hi.Key,
hi.Val.(Handler), hi.Val,
} }
handlers <- ht handlers <- ht
} }
@ -218,35 +218,25 @@ func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
var stack []execStack var stack []execStack
// Get internal handlers first. // Get internal handlers first.
ihm, iok := c.internal.cm.Get(command) hmap, iok := c.internal.cm.Get(command)
if iok { if iok {
hmap := ihm.(cmap.ConcurrentMap)
for assigned := range hmap.IterBuffered() { for assigned := range hmap.IterBuffered() {
cuid := assigned.Key cuid := assigned.Key
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) { if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
continue continue
} }
hi, _ := hmap.Get(cuid) hndlr, _ := hmap.Get(cuid)
hndlr, ok := hi.(Handler)
if !ok {
panic("improper handler type in map")
}
stack = append(stack, execStack{hndlr, cuid}) stack = append(stack, execStack{hndlr, cuid})
} }
} }
// Then external handlers. // Then external handlers.
ehm, eok := c.external.cm.Get(command) hmap, eok := c.external.cm.Get(command)
if eok { if eok {
hmap := ehm.(cmap.ConcurrentMap)
for _, cuid := range hmap.Keys() { for _, cuid := range hmap.Keys() {
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) { if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
continue continue
} }
hi, _ := hmap.Get(cuid) hndlr, _ := hmap.Get(cuid)
hndlr, ok := hi.(Handler)
if !ok {
panic("improper handler type in map")
}
stack = append(stack, execStack{hndlr, cuid}) stack = append(stack, execStack{hndlr, cuid})
} }
} }
@ -334,14 +324,12 @@ func (c *Caller) remove(cuid string) (ok bool) {
} }
// Check if the irc command/event has any handlers on it. // Check if the irc command/event has any handlers on it.
var h interface{} var hs cmap.ConcurrentMap[string, Handler]
h, ok = c.external.cm.Get(cmd) hs, ok = c.external.cm.Get(cmd)
if !ok { if !ok {
return return
} }
hs := h.(cmap.ConcurrentMap)
// Check to see if it's actually a registered handler. // Check to see if it's actually a registered handler.
if _, ok = hs.Get(cuid); !ok { if _, ok = hs.Get(cuid); !ok {
return return
@ -378,8 +366,7 @@ func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid
var ( var (
parent *nestedHandlers parent *nestedHandlers
chandlers cmap.ConcurrentMap chandlers cmap.ConcurrentMap[string, Handler]
ei interface{}
ok bool ok bool
) )
@ -389,12 +376,10 @@ func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid
parent = c.external parent = c.external
} }
ei, ok = parent.cm.Get(cmd) chandlers, ok = parent.cm.Get(cmd)
if ok { if !ok {
chandlers = ei.(cmap.ConcurrentMap) chandlers = cmap.New[Handler]()
} else {
chandlers = cmap.New()
} }
chandlers.Set(uid, handler) chandlers.Set(uid, handler)
@ -549,6 +534,8 @@ func (e *HandlerError) String() string {
// DefaultRecoverHandler can be used with Config.RecoverFunc as a default // 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 // 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. // debug log (see Config.Debug), or os.Stdout if Config.Debug is unset.
//
//goland:noinspection GoUnusedExportedFunction
func DefaultRecoverHandler(client *Client, err *HandlerError) { func DefaultRecoverHandler(client *Client, err *HandlerError) {
if client.Config.Debug == nil { if client.Config.Debug == nil {
fmt.Println(err.Error()) fmt.Println(err.Error())

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
}

@ -7,7 +7,8 @@ package girc
import ( import (
"encoding/json" "encoding/json"
"strings" "strings"
"sync"
cmap "github.com/orcaman/concurrent-map/v2"
) )
// CMode represents a single step of a given mode change. // 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?: // 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. // A = Mode that adds or removes a nick or address to a list. Always has a parameter.
// C = Mode that changes a setting and only has a parameter when set. // B = Mode that changes a setting and always has a parameter.
// D = Mode that changes a setting and never has a parameter. // C = Mode that changes a setting and only has a parameter when set.
// Note: Modes of type A return the list when there is no parameter present. // D = Mode that changes a setting and never has a parameter.
// Note: Some clients assumes that any mode not listed is of type D. // Note: Modes of type A return the list when there is no parameter present.
// Note: Modes in PREFIX are not listed but could be considered type B. // 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) { func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) {
if len(c.raw) < 1 { if len(c.raw) < 1 {
return false, true return false, true
@ -368,13 +370,9 @@ func handleMODE(c *Client, e Event) {
// chanModes returns the ISUPPORT list of server-supported channel modes, // chanModes returns the ISUPPORT list of server-supported channel modes,
// alternatively falling back to ModeDefaults. // alternatively falling back to ModeDefaults.
func (s *state) chanModes() string { func (s *state) chanModes() string {
if validmodes, ok := s.serverOptions.Get("CHANMODES"); ok { if validmodes, ok := s.serverOptions.Get("CHANMODES"); ok && IsValidChannelMode(validmodes) {
modes := validmodes.(string) return validmodes
if IsValidChannelMode(modes) {
return modes
}
} }
return ModeDefaults return ModeDefaults
} }
@ -382,63 +380,47 @@ func (s *state) chanModes() string {
// This includes mode characters, as well as user prefix symbols. Falls back // This includes mode characters, as well as user prefix symbols. Falls back
// to DefaultPrefixes if not server-supported. // to DefaultPrefixes if not server-supported.
func (s *state) userPrefixes() string { func (s *state) userPrefixes() string {
if pi, ok := s.serverOptions.Get("PREFIX"); ok { if prefix, ok := s.serverOptions.Get("PREFIX"); ok && isValidUserPrefix(prefix) {
prefix := pi.(string) return prefix
if isValidUserPrefix(prefix) {
return prefix
}
} }
return DefaultPrefixes return DefaultPrefixes
} }
// UserPerms contains all of the permissions for each channel the user is // UserPerms contains all of the permissions for each channel the user is
// in. // in.
type UserPerms struct { type UserPerms struct {
mu sync.RWMutex channels cmap.ConcurrentMap[string, *Perms]
channels map[string]Perms
} }
// Copy returns a deep copy of the channel permissions. // Copy returns a deep copy of the channel permissions.
func (p *UserPerms) Copy() (perms *UserPerms) { func (p *UserPerms) Copy() (perms *UserPerms) {
np := &UserPerms{ np := &UserPerms{
channels: make(map[string]Perms), channels: cmap.New[*Perms](),
} }
for key := range p.channels { for tuple := range p.channels.IterBuffered() {
np.channels[key] = p.channels[key] np.channels.Set(tuple.Key, tuple.Val)
} }
return np return np
} }
// MarshalJSON implements json.Marshaler. // MarshalJSON implements json.Marshaler.
func (p *UserPerms) MarshalJSON() ([]byte, error) { func (p *UserPerms) MarshalJSON() ([]byte, error) {
p.mu.Lock()
out, err := json.Marshal(&p.channels) out, err := json.Marshal(&p.channels)
p.mu.Unlock()
return out, err return out, err
} }
// Lookup looks up the users permissions for a given channel. ok is false // Lookup looks up the users permissions for a given channel. ok is false
// if the user is not in the given channel. // if the user is not in the given channel.
func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) { func (p *UserPerms) Lookup(channel string) (perms *Perms, ok bool) {
p.mu.RLock() return p.channels.Get(ToRFC1459(channel))
defer p.mu.RUnlock()
perms, ok = p.channels[ToRFC1459(channel)]
return perms, ok
} }
func (p *UserPerms) set(channel string, perms Perms) { func (p *UserPerms) set(channel string, perms *Perms) {
p.mu.Lock() p.channels.Set(ToRFC1459(channel), perms)
p.channels[ToRFC1459(channel)] = perms
p.mu.Unlock()
} }
func (p *UserPerms) remove(channel string) { func (p *UserPerms) remove(channel string) {
p.mu.Lock() p.channels.Remove(ToRFC1459(channel))
delete(p.channels, ToRFC1459(channel))
p.mu.Unlock()
} }
// Perms contains all channel-based user permissions. The minimum op, and // 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 // IsAdmin indicates that the user has banning abilities, and are likely a
// very trustable user (e.g. op+). // very trustable user (e.g. op+).
func (m Perms) IsAdmin() bool { func (m *Perms) IsAdmin() bool {
if m.Owner || m.Admin || m.Op { if m.Owner || m.Admin || m.Op {
return true return true
} }
@ -474,7 +456,7 @@ func (m Perms) IsAdmin() bool {
// IsTrusted indicates that the user at least has modes set upon them, higher // IsTrusted indicates that the user at least has modes set upon them, higher
// than a regular joining user. // than a regular joining user.
func (m Perms) IsTrusted() bool { func (m *Perms) IsTrusted() bool {
if m.IsAdmin() || m.HalfOp || m.Voice { if m.IsAdmin() || m.HalfOp || m.Voice {
return true return true
} }

146
state.go

@ -10,7 +10,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
cmap "github.com/orcaman/concurrent-map" cmap "github.com/orcaman/concurrent-map/v2"
) )
// state represents the actively-changing variables within the client // state represents the actively-changing variables within the client
@ -22,12 +22,13 @@ type state struct {
nick, ident, host atomic.Value nick, ident, host atomic.Value
// channels represents all channels we're active in. // channels represents all channels we're active in.
// channels map[string]*Channel // channels map[string]*Channel
channels cmap.ConcurrentMap channels cmap.ConcurrentMap[string, *Channel]
// users represents all of users that we're tracking. // users represents all of users that we're tracking.
// users map[string]*User // users map[string]*User
users cmap.ConcurrentMap users cmap.ConcurrentMap[string, *User]
// enabledCap are the capabilities which are enabled for this connection. // 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 // 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 check. These will get sent once we have received the
// last capability list command from the server. // last capability list command from the server.
@ -35,7 +36,8 @@ type state struct {
// serverOptions are the standard capabilities and configurations // serverOptions are the standard capabilities and configurations
// supported by the server at connection time. This also includes // supported by the server at connection time. This also includes
// RPL_ISUPPORT entries. // RPL_ISUPPORT entries.
serverOptions cmap.ConcurrentMap // serverOptions map[string]string
serverOptions cmap.ConcurrentMap[string, string]
// network is an alternative way to store and retrieve the NETWORK server option. // network is an alternative way to store and retrieve the NETWORK server option.
network atomic.Value network atomic.Value
@ -54,22 +56,31 @@ type state struct {
sts strictTransport sts strictTransport
} }
type Clearer interface {
Clear()
}
// reset resets the state back to it's original form. // reset resets the state back to it's original form.
func (s *state) reset(initial bool) { func (s *state) reset(initial bool) {
s.nick.Store("") s.nick.Store("")
s.ident.Store("") s.ident.Store("")
s.host.Store("") s.host.Store("")
s.network.Store("") s.network.Store("")
var cmaps = []*cmap.ConcurrentMap{&s.channels, &s.users, &s.serverOptions} var cmaps = []Clearer{&s.channels, &s.users, &s.serverOptions}
for _, cm := range cmaps { for i, cm := range cmaps {
if initial { switch {
*cm = cmap.New() case i == 0 && initial:
} else { cm = cmap.New[*Channel]()
case i == 1 && initial:
cm = cmap.New[*User]()
case i == 2 && initial:
cm = cmap.New[string]()
default:
cm.Clear() cm.Clear()
} }
} }
s.enabledCap = make(map[string]map[string]string) s.enabledCap = cmap.New[map[string]string]()
s.tmpCap = make(map[string]map[string]string) s.tmpCap = make(map[string]map[string]string)
s.motd = "" s.motd = ""
@ -81,11 +92,11 @@ func (s *state) reset(initial bool) {
// User represents an IRC user and the state attached to them. // User represents an IRC user and the state attached to them.
type User struct { type User struct {
// Nick is the users current nickname. rfc1459 compliant. // 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 // Ident is the users username/ident. Ident is commonly prefixed with a
// "~", which indicates that they do not have a identd server setup for // "~", which indicates that they do not have a identd server setup for
// authentication. // authentication.
Ident string `json:"ident"` Ident *MarshalableAtomicValue `json:"ident"`
// Host is the visible host of the users connection that the server has // 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 // provided to us for their connection. May not always be accurate due to
// many networks spoofing/hiding parts of the hostname for privacy // many networks spoofing/hiding parts of the hostname for privacy
@ -93,7 +104,7 @@ type User struct {
Host string `json:"host"` Host string `json:"host"`
// Mask is the combined Nick!Ident@Host of the given user. // Mask is the combined Nick!Ident@Host of the given user.
Mask string `json:"mask"` Mask *MarshalableAtomicValue `json:"mask"`
// Network is the name of the IRC network where this user was found. // 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. // This has been added for the purposes of girc being used in multi-client scenarios with data persistence.
@ -106,8 +117,8 @@ type User struct {
// //
// NOTE: If the ChannelList is empty for the user, then the user's info could be out of date. // 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! // turns out Concurrent-Map implements json.Marhsal!
// https://github.com/orcaman/concurrent-map/blob/893feb299719d9cbb2cfbe08b6dd4eb567d8039d/concurrent_map.go#L305 // https://github.com/orcaman/concurrent-map/v2/blob/893feb299719d9cbb2cfbe08b6dd4eb567d8039d/concurrent_map.go#L305
ChannelList cmap.ConcurrentMap `json:"channels"` ChannelList cmap.ConcurrentMap[string, *Channel] `json:"channels"`
// FirstSeen represents the first time that the user was seen by the // 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. // client for the given channel. Only usable if from state, not in past.
@ -143,10 +154,8 @@ type User struct {
} `json:"extras"` } `json:"extras"`
} }
// Channels returns a reference of *Channels that the client knows the user // Channels returns a slice of pointers to Channel types that the client knows the user is in.
// is in. If you're just looking for the namme of the channels, use func (u *User) Channels(c *Client) []*Channel {
// User.ChannelList.
func (u User) Channels(c *Client) []*Channel {
if c == nil { if c == nil {
panic("nil Client provided") panic("nil Client provided")
} }
@ -154,8 +163,8 @@ func (u User) Channels(c *Client) []*Channel {
var channels []*Channel var channels []*Channel
for listed := range u.ChannelList.IterBuffered() { for listed := range u.ChannelList.IterBuffered() {
chn, chok := listed.Val.(*Channel) chn := listed.Val
if chok { if chn != nil {
channels = append(channels, chn) channels = append(channels, chn)
continue continue
} }
@ -180,7 +189,9 @@ func (u *User) Copy() *User {
*nu = *u *nu = *u
nu.Perms = u.Perms.Copy() 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 return nu
} }
@ -199,7 +210,7 @@ func (u *User) addChannel(name string, chn *Channel) {
u.ChannelList.Set(name, chn) u.ChannelList.Set(name, chn)
u.Perms.set(name, Perms{}) u.Perms.set(name, &Perms{})
} }
// deleteChannel removes an existing channel from the users channel list. // deleteChannel removes an existing channel from the users channel list.
@ -248,7 +259,7 @@ type Channel struct {
Created string `json:"created"` Created string `json:"created"`
// UserList is a sorted list of all users we are currently tracking within // UserList is a sorted list of all users we are currently tracking within
// the channel. Each is the1 nickname, and is rfc1459 compliant. // the channel. Each is the1 nickname, and is rfc1459 compliant.
UserList cmap.ConcurrentMap `json:"user_list"` UserList cmap.ConcurrentMap[string, *User] `json:"user_list"`
// Network is the name of the IRC network where this channel was found. // 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. // This has been added for the purposes of girc being used in multi-client scenarios with data persistence.
Network string `json:"network"` Network string `json:"network"`
@ -260,7 +271,7 @@ type Channel struct {
// Users returns a reference of *Users that the client knows the channel has // 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. // 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 { if c == nil {
panic("nil Client provided") panic("nil Client provided")
} }
@ -280,7 +291,7 @@ func (ch Channel) Users(c *Client) []*User {
// Trusted returns a list of users which have voice or greater in the given // Trusted returns a list of users which have voice or greater in the given
// channel. See Perms.IsTrusted() for more information. // channel. See Perms.IsTrusted() for more information.
func (ch Channel) Trusted(c *Client) []*User { func (ch *Channel) Trusted(c *Client) []*User {
if c == nil { if c == nil {
panic("nil Client provided") panic("nil Client provided")
} }
@ -305,7 +316,7 @@ func (ch Channel) Trusted(c *Client) []*User {
// Admins returns a list of users which have half-op (if supported), or // Admins returns a list of users which have half-op (if supported), or
// greater permissions (op, admin, owner, etc) in the given channel. See // greater permissions (op, admin, owner, etc) in the given channel. See
// Perms.IsAdmin() for more information. // Perms.IsAdmin() for more information.
func (ch Channel) Admins(c *Client) []*User { func (ch *Channel) Admins(c *Client) []*User {
if c == nil { if c == nil {
panic("nil Client provided") panic("nil Client provided")
} }
@ -314,19 +325,17 @@ func (ch Channel) Admins(c *Client) []*User {
for listed := range ch.UserList.IterBuffered() { for listed := range ch.UserList.IterBuffered() {
ui := listed.Val ui := listed.Val
user, usrok := ui.(*User)
if !usrok { if ui == nil {
user = c.state.lookupUser(listed.Key) if ui = c.state.lookupUser(listed.Key); ui == nil {
if user == nil {
continue continue
} else {
ch.UserList.Set(listed.Key, user)
} }
ch.UserList.Set(listed.Key, ui)
} }
perms, ok := user.Perms.Lookup(ch.Name) perms, ok := ui.Perms.Lookup(ch.Name)
if ok && perms.IsAdmin() { if ok && perms.IsAdmin() {
users = append(users, user) users = append(users, ui)
} }
} }
@ -356,7 +365,9 @@ func (ch *Channel) Copy() *Channel {
nc := &Channel{} nc := &Channel{}
*nc = *ch *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. // And modes.
nc.Modes = ch.Modes.Copy() nc.Modes = ch.Modes.Copy()
@ -393,7 +404,7 @@ func (s *state) createChannel(name string) (ok bool) {
s.channels.Set(ToRFC1459(name), &Channel{ s.channels.Set(ToRFC1459(name), &Channel{
Name: name, Name: name,
UserList: cmap.New(), UserList: cmap.New[*User](),
Joined: time.Now(), Joined: time.Now(),
Network: s.client.NetworkName(), Network: s.client.NetworkName(),
Modes: NewCModes(supported, prefixes), Modes: NewCModes(supported, prefixes),
@ -406,17 +417,14 @@ func (s *state) createChannel(name string) (ok bool) {
func (s *state) deleteChannel(name string) { func (s *state) deleteChannel(name string) {
name = ToRFC1459(name) name = ToRFC1459(name)
c, ok := s.channels.Get(name) chn, ok := s.channels.Get(name)
if !ok { if !ok {
return return
} }
chn := c.(*Channel)
for listed := range chn.UserList.IterBuffered() { for listed := range chn.UserList.IterBuffered() {
ui, _ := s.users.Get(listed.Key) usr, uok := s.users.Get(listed.Key)
usr, usrok := ui.(*User) if uok {
if usrok {
usr.deleteChannel(name) usr.deleteChannel(name)
} }
} }
@ -428,42 +436,55 @@ func (s *state) deleteChannel(name string) {
// found. // found.
func (s *state) lookupChannel(name string) *Channel { func (s *state) lookupChannel(name string) *Channel {
ci, cok := s.channels.Get(ToRFC1459(name)) ci, cok := s.channels.Get(ToRFC1459(name))
chn, ok := ci.(*Channel) if ci == nil || !cok {
if !ok || !cok {
return nil return nil
} }
return chn return ci
} }
// lookupUser returns a reference to a user, nil returned if no results // lookupUser returns a reference to a user, nil returned if no results
// found. // found.
func (s *state) lookupUser(name string) *User { func (s *state) lookupUser(name string) *User {
ui, uok := s.users.Get(ToRFC1459(name)) usr, uok := s.users.Get(ToRFC1459(name))
usr, ok := ui.(*User) if usr == nil || !uok {
if !ok || !uok {
return nil return nil
} }
return usr return usr
} }
func (s *state) createUser(src *Source) (u *User, ok bool) { func (s *state) createUser(src *Source) (u *User, ok bool) {
if _, ok := s.users.Get(src.ID()); ok { if u, ok = s.users.Get(src.ID()); ok {
// User already exists. // User already exists.
return nil, false return u, false
}
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)
} }
u = &User{ u = &User{
Nick: src.Name, Nick: NewAtomicString(src.Name),
Host: src.Host, Host: src.Host,
Ident: src.Ident, Ident: NewAtomicString(src.Ident),
Mask: src.Name + "!" + src.Ident + "@" + src.Host, Mask: NewAtomicString(mask.String()),
ChannelList: cmap.New(), ChannelList: cmap.New[*Channel](),
FirstSeen: time.Now(), FirstSeen: time.Now(),
LastActive: time.Now(), LastActive: time.Now(),
Network: s.client.NetworkName(), Network: s.client.NetworkName(),
Perms: &UserPerms{channels: make(map[string]Perms)}, Perms: &UserPerms{channels: cmap.New[*Perms]()},
} }
strs.MustPut(mask)
s.users.Set(src.ID(), u) s.users.Set(src.ID(), u)
return u, true return u, true
} }
@ -514,20 +535,19 @@ func (s *state) renameUser(from, to string) {
} }
if old != nil && user == nil { if old != nil && user == nil {
user = old.(*User) user = old
} }
user.Nick = to user.Nick.Store(to)
user.LastActive = time.Now() user.LastActive = time.Now()
s.users.Set(ToRFC1459(to), user) s.users.Set(ToRFC1459(to), user)
for chanchan := range s.channels.IterBuffered() { for chanchan := range s.channels.IterBuffered() {
chi := chanchan.Val chn := chanchan.Val
chn, chok := chi.(*Channel) if chn == nil {
if !chok {
continue continue
} }
if old, oldok := chn.UserList.Pop(from); oldok { if old, oldok = chn.UserList.Pop(from); oldok {
chn.UserList.Set(to, old) chn.UserList.Set(to, old)
} }
} }

@ -123,7 +123,7 @@ func TestState(t *testing.T) {
fullUsers := c.Users() fullUsers := c.Users()
for i := 0; i < len(fullUsers); i++ { for i := 0; i < len(fullUsers); i++ {
if fullUsers[i].Nick != users[i] { 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) t.Errorf("fullUsers nick doesn't map to same nick in UsersList: %q :: %#v", fullUsers[i].Nick, users)
return return
} }
@ -136,14 +136,14 @@ func TestState(t *testing.T) {
} }
adm := ch.Admins(c) adm := ch.Admins(c)
admList := []string{} var admList []string
for i := 0; i < len(adm); i++ { 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) trusted := ch.Trusted(c)
trustedList := []string{} var trustedList []string
for i := 0; i < len(trusted); i++ { 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"}) { if !reflect.DeepEqual(admList, []string{"nick2"}) {
@ -213,7 +213,7 @@ func TestState(t *testing.T) {
return return
} }
if user.Nick != "fhjones" { if user.Nick.Load().(string) != "fhjones" {
t.Errorf("User.Nick == %q, wanted \"nick\"", user.Nick) t.Errorf("User.Nick == %q, wanted \"nick\"", user.Nick)
return return
} }
@ -228,7 +228,7 @@ func TestState(t *testing.T) {
return return
} }
if user.Ident != "~user" { if user.Ident.Load().(string) != "~user" {
t.Errorf("User.Ident == %q, wanted \"~user\"", user.Ident) t.Errorf("User.Ident == %q, wanted \"~user\"", user.Ident)
return return
} }
@ -250,7 +250,9 @@ func TestState(t *testing.T) {
bounceStart <- true 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)) _, err := conn.Write([]byte(mockConnStartState))
if err != nil { if err != nil {
panic(err) panic(err)
@ -282,10 +284,9 @@ func TestState(t *testing.T) {
return return
} }
chi, chnok := user.ChannelList.Get("#channel") chn, chnok := user.ChannelList.Get("#channel")
chn, chiok := chi.(*Channel)
if !chnok || !chiok { if !chnok {
t.Errorf("should have been able to get a pointer by looking up #channel") t.Errorf("should have been able to get a pointer by looking up #channel")
return return
} }
@ -295,8 +296,7 @@ func TestState(t *testing.T) {
return return
} }
chi2, _ := user.ChannelList.Get("#channel2") chn2, _ := user.ChannelList.Get("#channel2")
chn2, _ := chi2.(*Channel)
if chn2.Len() != len([]string{"notjones"}) { if chn2.Len() != len([]string{"notjones"}) {
t.Errorf("channel.UserList.Count() == %d, wanted %d", t.Errorf("channel.UserList.Count() == %d, wanted %d",
@ -316,7 +316,9 @@ func TestState(t *testing.T) {
bounceEnd <- true 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)) _, err = conn.Write([]byte(mockConnEndState))
if err != nil { if err != nil {
panic(err) panic(err)

11
util.go

@ -1,11 +0,0 @@
package girc
import (
"math/rand"
"time"
)
func randSleep() {
rand.Seed(time.Now().UnixNano())
time.Sleep(time.Duration(rand.Intn(25)) * time.Millisecond)
}

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}
}