Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e1fa2f1dc | |||
98424443a8 |
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
@ -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.18
|
go-version: 1.17
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v ./...
|
run: go build -v ./...
|
||||||
|
23
.github/workflows/test.yml
vendored
Normal file
23
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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
2
.gitignore
vendored
@ -1,4 +1,2 @@
|
|||||||
.idea
|
.idea
|
||||||
*.save
|
*.save
|
||||||
*.swp
|
|
||||||
corpus/
|
|
||||||
|
22
README.md
22
README.md
@ -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)~~ a total destruction of a 100 mile radius.
|
- Only requires [standard library packages](https://godoc.org/github.com/yunginnanet/girc-atomic?imports)
|
||||||
- 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,11 +37,25 @@
|
|||||||
- 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 atomics and concurrent maps to reduce backpressure in multi-client usage. (fork)
|
- Utilizes the atomic/value package from stdlib to reduce backpressure in multi-client usage.
|
||||||
- Additional CTCP handlers and customization. (fork)
|
- Additional CTCP handlers and customization.
|
||||||
- ??????
|
- ??????
|
||||||
- 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
|
||||||
@ -75,7 +89,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
|
||||||
|
|
||||||
...and then later defiled by [some idiot](https://github.com/yunginnanet).
|
later defiled by [some idiot](https://github.com/yunginnanet).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
29
builtin.go
29
builtin.go
@ -18,6 +18,9 @@ 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))
|
||||||
@ -126,8 +129,6 @@ 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() + "_")
|
||||||
@ -145,7 +146,6 @@ 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.Load().(string), user)
|
channel.addUser(user.Nick, user)
|
||||||
user.addChannel(channel.Name, channel)
|
user.addChannel(channel.Name, channel)
|
||||||
|
|
||||||
// Assume extended-join (ircv3).
|
// Assume extended-join (ircv3).
|
||||||
@ -216,6 +216,9 @@ 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])
|
||||||
|
|
||||||
@ -232,7 +235,9 @@ 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)
|
||||||
}
|
}
|
||||||
@ -243,6 +248,7 @@ 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
|
||||||
@ -345,17 +351,8 @@ func handleWHO(c *Client, e Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.Host = host
|
user.Host = host
|
||||||
user.Ident.Store(ident)
|
user.Ident = 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" {
|
||||||
@ -556,7 +553,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 c.state.motd != "" {
|
if len(c.state.motd) != 0 {
|
||||||
c.state.motd += "\n"
|
c.state.motd += "\n"
|
||||||
}
|
}
|
||||||
c.state.motd += e.Last()
|
c.state.motd += e.Last()
|
||||||
|
17
cap.go
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 negotiation).
|
// sts negotation).
|
||||||
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,7 +122,8 @@ 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 {
|
||||||
c.state.enabledCap.Remove(capab)
|
// TODO: test the deletion.
|
||||||
|
delete(c.state.enabledCap, capab)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -193,10 +194,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.Set(capab, val)
|
c.state.enabledCap[capab] = val
|
||||||
continue
|
} else {
|
||||||
|
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,
|
||||||
@ -204,7 +205,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.Get("sts"); sok && !c.Config.DisableSTS {
|
if sts, sok := c.state.enabledCap["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.
|
||||||
@ -284,7 +285,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.Get("sasl"); ok && c.Config.SASL != nil {
|
if _, ok := c.state.enabledCap["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
|
||||||
@ -307,7 +308,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.Store(e.Params[0])
|
user.Ident = e.Params[0]
|
||||||
user.Host = e.Params[1]
|
user.Host = e.Params[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
cap_tags.go
11
cap_tags.go
@ -51,12 +51,9 @@ 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.
|
||||||
@ -252,6 +249,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
44
cap_test.go
44
cap_test.go
@ -34,36 +34,26 @@ func TestCapSupported(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var testsParseCap = []struct {
|
func TestParseCap(t *testing.T) {
|
||||||
in string
|
tests := []struct {
|
||||||
want map[string]map[string]string
|
in string
|
||||||
}{
|
want map[string]map[string]string
|
||||||
{in: "sts=port=6697,duration=1234567890,preload", want: map[string]map[string]string{"sts": {"duration": "1234567890", "preload": "", "port": "6697"}}},
|
}{
|
||||||
{in: "userhost-in-names", want: map[string]map[string]string{"userhost-in-names": nil}},
|
{in: "sts=port=6697,duration=1234567890,preload", want: map[string]map[string]string{"sts": {"duration": "1234567890", "preload": "", "port": "6697"}}},
|
||||||
{in: "userhost-in-names test2", want: map[string]map[string]string{"userhost-in-names": nil, "test2": nil}},
|
{in: "userhost-in-names", want: map[string]map[string]string{"userhost-in-names": nil}},
|
||||||
{in: "example/name=test", want: map[string]map[string]string{"example/name": {"test": ""}}},
|
{in: "userhost-in-names test2", want: map[string]map[string]string{"userhost-in-names": nil, "test2": nil}},
|
||||||
{
|
{in: "example/name=test", want: map[string]map[string]string{"example/name": {"test": ""}}},
|
||||||
in: "userhost-in-names example/name example/name2=test=1,test2=true",
|
{
|
||||||
want: map[string]map[string]string{
|
in: "userhost-in-names example/name example/name2=test=1,test2=true",
|
||||||
"userhost-in-names": nil,
|
want: map[string]map[string]string{
|
||||||
"example/name": nil,
|
"userhost-in-names": nil,
|
||||||
"example/name2": {"test": "1", "test2": "true"},
|
"example/name": nil,
|
||||||
|
"example/name2": {"test": "1", "test2": "true"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzParseCap(f *testing.F) {
|
|
||||||
for _, tc := range testsParseCap {
|
|
||||||
f.Add(tc.in)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, orig string) {
|
for _, tt := range tests {
|
||||||
_ = 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) {
|
||||||
|
61
client.go
61
client.go
@ -11,6 +11,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@ -21,8 +22,6 @@ 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
|
||||||
@ -224,13 +223,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
|
||||||
@ -276,10 +275,10 @@ func (conf *Config) isValid() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !IsValidNick(conf.Nick) {
|
if !IsValidNick(conf.Nick) {
|
||||||
return &ErrInvalidConfig{Conf: *conf, err: fmt.Errorf("bad nickname specified: %s", conf.Nick)}
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified: " + conf.Nick)}
|
||||||
}
|
}
|
||||||
if !IsValidUser(conf.User) {
|
if !IsValidUser(conf.User) {
|
||||||
return &ErrInvalidConfig{Conf: *conf, err: fmt.Errorf("bad user/ident specified: %s", conf.Nick)}
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified: " + conf.User)}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -321,7 +320,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(io.Discard, "", 0)
|
c.debug = log.New(ioutil.Discard, "", 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if envDebug {
|
if envDebug {
|
||||||
@ -342,12 +341,7 @@ 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)
|
||||||
|
|
||||||
@ -607,7 +601,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
|
chn := channel.Val.(*Channel)
|
||||||
if !chn.UserIn(c.GetNick()) {
|
if !chn.UserIn(c.GetNick()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -623,9 +617,9 @@ func (c *Client) ChannelList() []string {
|
|||||||
func (c *Client) Channels() []*Channel {
|
func (c *Client) Channels() []*Channel {
|
||||||
c.panicIfNotTracking()
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
channels := make([]*Channel, 0, c.state.channels.Count())
|
channels := make([]*Channel, 0, len(c.state.channels))
|
||||||
for channel := range c.state.channels.IterBuffered() {
|
for channel := range c.state.channels.IterBuffered() {
|
||||||
chn := channel.Val
|
chn := channel.Val.(*Channel)
|
||||||
channels = append(channels, chn.Copy())
|
channels = append(channels, chn.Copy())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,13 +634,13 @@ func (c *Client) Channels() []*Channel {
|
|||||||
func (c *Client) UserList() []string {
|
func (c *Client) UserList() []string {
|
||||||
c.panicIfNotTracking()
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
users := make([]string, 0, c.state.users.Count())
|
users := make([]string, 0, len(c.state.users))
|
||||||
for user := range c.state.users.IterBuffered() {
|
for user := range c.state.users.IterBuffered() {
|
||||||
usr := user.Val
|
usr := user.Val.(*User)
|
||||||
if usr.Stale {
|
if usr.Stale {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
users = append(users, usr.Nick.Load().(string))
|
users = append(users, usr.Nick)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(users)
|
sort.Strings(users)
|
||||||
@ -658,14 +652,14 @@ func (c *Client) UserList() []string {
|
|||||||
func (c *Client) Users() []*User {
|
func (c *Client) Users() []*User {
|
||||||
c.panicIfNotTracking()
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
users := make([]*User, 0, c.state.users.Count())
|
users := make([]*User, 0, len(c.state.users))
|
||||||
for user := range c.state.users.IterBuffered() {
|
for user := range c.state.users.IterBuffered() {
|
||||||
usr := user.Val
|
usr := user.Val.(*User)
|
||||||
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.Load().(string) < users[j].Nick.Load().(string)
|
return users[i].Nick < users[j].Nick
|
||||||
})
|
})
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
@ -709,15 +703,18 @@ 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()
|
||||||
|
|
||||||
result, ok = c.state.serverOptions.Get(key)
|
oi, 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
|
||||||
}
|
}
|
||||||
@ -730,7 +727,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
|
o[opt.Key] = opt.Val.(string)
|
||||||
}
|
}
|
||||||
jcytes, _ := json.Marshal(o)
|
jcytes, _ := json.Marshal(o)
|
||||||
return jcytes
|
return jcytes
|
||||||
@ -803,13 +800,15 @@ func (c *Client) HasCapability(name string) (has bool) {
|
|||||||
|
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
|
|
||||||
for capab := range c.state.enabledCap.IterBuffered() {
|
c.state.RLock()
|
||||||
key := strings.ToLower(capab.Key)
|
for key := range c.state.enabledCap {
|
||||||
|
key = strings.ToLower(key)
|
||||||
if key == name {
|
if key == name {
|
||||||
has = true
|
has = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
return has
|
return has
|
||||||
}
|
}
|
||||||
@ -851,7 +850,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,24 +93,14 @@ func TestClientLifetime(t *testing.T) {
|
|||||||
|
|
||||||
func TestClientUptime(t *testing.T) {
|
func TestClientUptime(t *testing.T) {
|
||||||
c, conn, server := genMockConn()
|
c, conn, server := genMockConn()
|
||||||
defer func() {
|
defer conn.Close()
|
||||||
if err := conn.Close(); err != nil {
|
defer server.Close()
|
||||||
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 func() {
|
go c.MockConnect(server)
|
||||||
if err := c.MockConnect(server); err != nil {
|
|
||||||
t.Errorf("failed to connect: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -148,24 +138,14 @@ func TestClientUptime(t *testing.T) {
|
|||||||
|
|
||||||
func TestClientGet(t *testing.T) {
|
func TestClientGet(t *testing.T) {
|
||||||
c, conn, server := genMockConn()
|
c, conn, server := genMockConn()
|
||||||
defer func() {
|
defer conn.Close()
|
||||||
if err := conn.Close(); err != nil {
|
defer server.Close()
|
||||||
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 func() {
|
go c.MockConnect(server)
|
||||||
if err := c.MockConnect(server); err != nil {
|
|
||||||
t.Errorf("failed to connect: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -189,14 +169,8 @@ func TestClientGet(t *testing.T) {
|
|||||||
|
|
||||||
func TestClientClose(t *testing.T) {
|
func TestClientClose(t *testing.T) {
|
||||||
c, conn, server := genMockConn()
|
c, conn, server := genMockConn()
|
||||||
defer func() {
|
defer server.Close()
|
||||||
if err := conn.Close(); err != nil {
|
defer conn.Close()
|
||||||
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,7 +4,6 @@ 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
|
||||||
|
91
commands.go
91
commands.go
@ -36,7 +36,7 @@ func (cmd *Commands) Join(channels ...string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer == "" {
|
if len(buffer) == 0 {
|
||||||
buffer = channels[i]
|
buffer = channels[i]
|
||||||
} else {
|
} else {
|
||||||
buffer += "," + channels[i]
|
buffer += "," + channels[i]
|
||||||
@ -111,8 +111,7 @@ 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{}) {
|
||||||
message := fmt.Sprintf(format, a...)
|
cmd.Message(target, fmt.Sprintf(Fmt(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
|
||||||
@ -120,83 +119,79 @@ 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) error {
|
func (cmd *Commands) Reply(event Event, message string) {
|
||||||
if event.Source == nil {
|
if event.Source == nil {
|
||||||
return ErrInvalidSource
|
panic(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 nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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) error {
|
func (cmd *Commands) ReplyKick(event Event, reason string) {
|
||||||
if event.Source == nil {
|
if event.Source == nil {
|
||||||
return ErrInvalidSource
|
panic(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) (err error) {
|
func (cmd *Commands) ReplyBan(event Event, reason string) {
|
||||||
if event.Source == nil {
|
if event.Source == nil {
|
||||||
return ErrInvalidSource
|
panic(ErrInvalidSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
if reason != "" {
|
if reason != "" {
|
||||||
err = cmd.Replyf(event, "{red}{b}[BAN] {r}%s", reason)
|
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], fmt.Sprintf("*!%s@%s", event.Source.Ident, event.Source.Host))
|
cmd.Ban(event.Params[0], event.Source.Name)
|
||||||
}
|
}
|
||||||
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.
|
||||||
// Formatted means both in the sense of Sprintf as well as girc style macros.
|
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) {
|
||||||
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
|
cmd.Reply(event, fmt.Sprintf(Fmt(format), a...))
|
||||||
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) error {
|
func (cmd *Commands) ReplyTo(event Event, message string) {
|
||||||
if event.Source == nil {
|
if event.Source == nil {
|
||||||
return ErrInvalidSource
|
panic(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)
|
||||||
} else {
|
return
|
||||||
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.
|
||||||
// Formatted means both in the sense of Sprintf as well as girc style macros.
|
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
|
||||||
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
|
cmd.ReplyTo(event, fmt.Sprintf(Fmt(format), a...))
|
||||||
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,
|
||||||
@ -220,9 +215,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). Formatted means both in the sense of Sprintf as well as girc styling codes.
|
// user).
|
||||||
func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
|
func (cmd *Commands) Noticef(target, format string, a ...interface{}) {
|
||||||
cmd.Notice(target, Fmt(fmt.Sprintf(format, a...)))
|
cmd.Notice(target, 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
|
||||||
@ -244,21 +239,14 @@ 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. Formatted means both in the sense of Sprintf as well as girc style macros.
|
// returns or newlines.
|
||||||
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
|
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
|
||||||
return cmd.SendRaw(Fmt(fmt.Sprintf(format, a...)))
|
return cmd.SendRaw(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)}})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,22 +286,6 @@ 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.
|
||||||
@ -321,6 +293,7 @@ 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}})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,7 +366,7 @@ func (cmd *Commands) List(channels ...string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer == "" {
|
if len(buffer) == 0 {
|
||||||
buffer = channels[i]
|
buffer = channels[i]
|
||||||
} else {
|
} else {
|
||||||
buffer += "," + channels[i]
|
buffer += "," + channels[i]
|
||||||
|
35
conn.go
35
conn.go
@ -12,8 +12,6 @@ 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
|
||||||
@ -57,8 +55,6 @@ 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 {
|
||||||
@ -73,11 +69,7 @@ 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
|
||||||
s := strs.Get()
|
local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0")
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -175,6 +167,18 @@ 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))
|
||||||
}
|
}
|
||||||
@ -509,13 +513,15 @@ 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 < c.state.enabledCap.Count(); i++ {
|
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||||
if _, ok := c.state.enabledCap.Get("message-tags"); ok {
|
if _, ok := c.state.enabledCap["message-tags"]; ok {
|
||||||
in = true
|
in = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
if !in {
|
if !in {
|
||||||
event.Tags = Tags{}
|
event.Tags = Tags{}
|
||||||
@ -589,7 +595,6 @@ 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 {
|
||||||
@ -604,8 +609,9 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, working *int32)
|
|||||||
past = true
|
past = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if pingSent && time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(180*time.Second) {
|
if time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(120*time.Second) {
|
||||||
// It's 180 seconds over what out ping delay is, connection has probably dropped.
|
// It's 60 seconds over what out ping delay is, connection
|
||||||
|
// 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)),
|
||||||
@ -623,7 +629,6 @@ 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, out *bytes.Buffer, irc *ircConn) {
|
func mockBuffers() (in *bytes.Buffer, 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, serverConn net.Conn) {
|
func genMockConn() (client *Client, clientConn net.Conn, 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
|
||||||
|
24
constants.go
24
constants.go
@ -5,8 +5,6 @@
|
|||||||
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"
|
||||||
@ -22,8 +20,6 @@ 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.
|
||||||
@ -37,8 +33,6 @@ 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
|
||||||
@ -54,8 +48,6 @@ 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
|
||||||
@ -64,8 +56,6 @@ 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
|
||||||
|
|
||||||
@ -85,8 +75,6 @@ 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"
|
||||||
@ -139,8 +127,6 @@ 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"
|
||||||
@ -284,8 +270,6 @@ 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"
|
||||||
@ -309,8 +293,6 @@ 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"
|
||||||
@ -331,8 +313,6 @@ 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"
|
||||||
@ -361,8 +341,6 @@ 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.
|
||||||
@ -373,8 +351,6 @@ 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
54
ctcp.go
@ -5,12 +5,11 @@
|
|||||||
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.
|
||||||
@ -105,8 +104,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 cmd == "" {
|
if len(cmd) <= 0 {
|
||||||
return cmd
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
out = string(ctcpDelim) + cmd
|
out = string(ctcpDelim) + cmd
|
||||||
@ -124,12 +123,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 cmap.ConcurrentMap[string, CTCPHandler]
|
handlers map[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: cmap.New[CTCPHandler]()}
|
return &CTCP{handlers: map[string]CTCPHandler{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// call executes the necessary CTCP handler for the incoming event/CTCP
|
// call executes the necessary CTCP handler for the incoming event/CTCP
|
||||||
@ -139,16 +138,27 @@ 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 val, ok := c.handlers.Get("*"); ok && val != nil {
|
if _, ok := c.handlers["*"]; ok {
|
||||||
val(client, *event)
|
c.handlers["*"](client, *event)
|
||||||
}
|
}
|
||||||
val, ok := c.handlers.Get(event.Command)
|
|
||||||
if !ok || val == nil || event.Command == CTCP_ACTION {
|
if _, ok := c.handlers[event.Command]; !ok {
|
||||||
|
// If ACTION, don't do anything.
|
||||||
|
if event.Command == CTCP_ACTION {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a ERRMSG reply, if we know who sent it.
|
||||||
|
if event.Source != nil && IsValidNick(event.Source.ID()) {
|
||||||
|
client.Cmd.SendCTCPReply(event.Source.ID(), CTCP_ERRMSG, "that is an unknown CTCP query")
|
||||||
|
}
|
||||||
return
|
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
|
||||||
@ -180,7 +190,10 @@ 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.handlers.Set(cmd, handler)
|
c.mu.Lock()
|
||||||
|
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,
|
||||||
@ -197,12 +210,18 @@ 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.handlers = cmap.New[CTCPHandler]()
|
c.mu.Lock()
|
||||||
|
c.handlers = map[string]CTCPHandler{}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Register necessary handlers.
|
// Register necessary handlers.
|
||||||
c.addDefaultHandlers()
|
c.addDefaultHandlers()
|
||||||
}
|
}
|
||||||
@ -292,7 +311,8 @@ 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)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
67
ctcp_test.go
67
ctcp_test.go
@ -9,40 +9,26 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var testsEncodeCTCP = []struct {
|
func TestEncodeCTCP(t *testing.T) {
|
||||||
name string
|
type args struct {
|
||||||
test *CTCPEvent
|
ctcp *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) {
|
tests := []struct {
|
||||||
got := EncodeCTCP(&CTCPEvent{Command: cmd, Text: text})
|
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: ""},
|
||||||
|
}
|
||||||
|
|
||||||
if utf8.ValidString(cmd) && utf8.ValidString(text) && !utf8.ValidString(got) {
|
for _, tt := range tests {
|
||||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
if got := EncodeCTCP(tt.args.ctcp); got != tt.want {
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodeCTCP(t *testing.T) {
|
|
||||||
for _, tt := range testsEncodeCTCP {
|
|
||||||
if got := EncodeCTCP(tt.test); 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,8 +110,7 @@ func TestCall(t *testing.T) {
|
|||||||
atomic.AddUint64(&counter, 1)
|
atomic.AddUint64(&counter, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
if ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}); atomic.LoadUint64(&counter) != 1 {
|
||||||
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")
|
||||||
@ -135,8 +120,7 @@ func TestCall(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
||||||
time.Sleep(250 * time.Millisecond)
|
if time.Sleep(250 * time.Millisecond); atomic.LoadUint64(&counter) != 2 {
|
||||||
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")
|
||||||
@ -145,15 +129,14 @@ func TestCall(t *testing.T) {
|
|||||||
atomic.AddUint64(&counter, 1)
|
atomic.AddUint64(&counter, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
if ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}); atomic.LoadUint64(&counter) != 3 {
|
||||||
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")
|
||||||
|
|
||||||
ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"})
|
if ctcp.call(New(Config{}), &CTCPEvent{Command: "TEST"}); atomic.LoadUint64(&counter) != 3 {
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,13 +145,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.Get("TEST"); ok {
|
if _, ok := ctcp.handlers["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.Get("TEST"); !ok {
|
if _, ok := ctcp.handlers["TEST"]; !ok {
|
||||||
t.Fatal("store: Set('TEST') didn't set")
|
t.Fatal("store: Set('TEST') didn't set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,7 +162,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.Get("TEST"); ok {
|
if _, ok := ctcp.handlers["TEST"]; ok {
|
||||||
t.Fatal("ctcp.Clear('TEST') didn't remove handler")
|
t.Fatal("ctcp.Clear('TEST') didn't remove handler")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,8 +174,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.Get("TEST1")
|
_, first := ctcp.handlers["TEST1"]
|
||||||
_, second := ctcp.handlers.Get("TEST2")
|
_, second := ctcp.handlers["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)
|
||||||
|
8
event.go
8
event.go
@ -52,7 +52,7 @@ func ParseEvent(raw string) (e *Event) {
|
|||||||
i = 0
|
i = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if raw != "" && raw[0] == messagePrefix {
|
if raw[0] == messagePrefix {
|
||||||
// Prefix ends with a space.
|
// Prefix ends with a space.
|
||||||
i = strings.IndexByte(raw, eventSpace)
|
i = strings.IndexByte(raw, eventSpace)
|
||||||
|
|
||||||
@ -313,9 +313,7 @@ func (e *Event) Bytes() []byte {
|
|||||||
buffer.Truncate(maxLength)
|
buffer.Truncate(maxLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we truncated in the middle of a utf8 character, we need to remove
|
out := buffer.Bytes()
|
||||||
// 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++ {
|
||||||
@ -640,7 +638,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 s.Ident == "" && s.Host == ""
|
return len(s.Ident) <= 0 && len(s.Host) <= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.Fatalf("ParseEvent: got nil, want: %s", tt.want)
|
t.Errorf("ParseEvent: got nil, want: %s", tt.want)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got.String() != tt.want {
|
if got.String() != tt.want {
|
||||||
@ -133,7 +133,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
format.go
41
format.go
@ -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.ReplaceAll(text, string(fmtOpenChar)+color+string(fmtCloseChar), "")
|
text = strings.Replace(text, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
|
||||||
}
|
}
|
||||||
for code := range fmtCodes {
|
for code := range fmtCodes {
|
||||||
text = strings.ReplaceAll(text, string(fmtOpenChar)+code+string(fmtCloseChar), "")
|
text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
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]?\d(,[019]?\d)?)?`)
|
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)
|
||||||
|
|
||||||
// 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.ReplaceAll(text, code, "")
|
text = strings.Replace(text, code, "", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 nick == "" {
|
if len(nick) <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,11 +253,10 @@ 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 )
|
||||||
// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
|
// ; any octet except NUL, CR, LF, " " and "@"
|
||||||
// ; any octet except NUL, CR, LF, " " and "@"
|
|
||||||
func IsValidUser(name string) bool {
|
func IsValidUser(name string) bool {
|
||||||
if name == "" {
|
if len(name) <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,7 +324,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 strings.EqualFold(input, match)
|
return input == match
|
||||||
}
|
}
|
||||||
|
|
||||||
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
|
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
|
||||||
|
450
format_test.go
450
format_test.go
@ -7,7 +7,6 @@ package girc
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkFormat(b *testing.B) {
|
func BenchmarkFormat(b *testing.B) {
|
||||||
@ -48,294 +47,207 @@ 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) {
|
||||||
for _, tt := range testsFormat {
|
type args struct {
|
||||||
if got := Fmt(tt.test); got != tt.want {
|
text string
|
||||||
t.Errorf("%s: Format(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var testsStripFormat = []struct {
|
|
||||||
name string
|
|
||||||
test string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{name: "start, end", test: "{red}test{c}", want: "test"},
|
|
||||||
{name: "start, middle, end", test: "{red}te{red}st{c}", want: "test"},
|
|
||||||
{name: "partial", test: "{redtest{c}", want: "{redtest"},
|
|
||||||
{name: "inside", test: "{re{c}d}test{c}", want: "{red}test"},
|
|
||||||
{name: "nothing", test: "this is a test.", want: "this is a test."},
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzStripFormat(f *testing.F) {
|
|
||||||
for _, tc := range testsStripFormat {
|
|
||||||
f.Add(tc.test)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, orig string) {
|
tests := []struct {
|
||||||
got := TrimFmt(orig)
|
name string
|
||||||
got2 := TrimFmt(got)
|
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"},
|
||||||
|
}
|
||||||
|
|
||||||
if utf8.ValidString(orig) {
|
for _, tt := range tests {
|
||||||
if !utf8.ValidString(got) {
|
if got := Fmt(tt.args.text); got != tt.want {
|
||||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
t.Errorf("%s: Format(%q) = %q, want %q", tt.name, tt.args.text, got, tt.want)
|
||||||
}
|
|
||||||
|
|
||||||
if !utf8.ValidString(got2) {
|
|
||||||
t.Errorf("produced invalid UTF-8 string %q", got2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStripFormat(t *testing.T) {
|
func TestStripFormat(t *testing.T) {
|
||||||
for _, tt := range testsStripFormat {
|
type args struct {
|
||||||
if got := TrimFmt(tt.test); got != tt.want {
|
text string
|
||||||
t.Errorf("%s: StripFormat(%q) = %q, want %q", tt.name, tt.test, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var testsStripRaw = []struct {
|
|
||||||
name string
|
|
||||||
test string // gets passed to Format() before sent
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{name: "start, end", test: "{red}{b}test{c}", want: "test"},
|
|
||||||
{name: "start, end in numbers", test: "{red}1234{c}", want: "1234"},
|
|
||||||
{name: "start, middle, end", test: "{red}te{red}st{c}", want: "test"},
|
|
||||||
{name: "partial", test: "{redtest{c}", want: "{redtest"},
|
|
||||||
{name: "inside", test: "{re{c}d}test{c}", want: "{red}test"},
|
|
||||||
{name: "fg+bg colors start", test: "{red,yellow}test{c}", want: "test"},
|
|
||||||
{name: "fg+bg colors start in numbers", test: "{red,yellow}1234{c}", want: "1234"},
|
|
||||||
{name: "fg+bg colors end", test: "test{,yellow}", want: "test"},
|
|
||||||
{name: "bg colors start", test: "{,yellow}test{c}", want: "test"},
|
|
||||||
{name: "inside", test: "{re{c}d}test{c}", want: "{red}test"},
|
|
||||||
{name: "nothing", test: "this is a test.", want: "this is a test."},
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzStripRaw(f *testing.F) {
|
|
||||||
for _, tc := range testsStripRaw {
|
|
||||||
f.Add(tc.test)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, orig string) {
|
tests := []struct {
|
||||||
got := StripRaw(orig)
|
name string
|
||||||
got2 := StripRaw(got)
|
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."},
|
||||||
|
}
|
||||||
|
|
||||||
if utf8.ValidString(orig) {
|
for _, tt := range tests {
|
||||||
if !utf8.ValidString(got) {
|
if got := TrimFmt(tt.args.text); got != tt.want {
|
||||||
t.Errorf("produced invalid UTF-8 string %q", got)
|
t.Errorf("%s: StripFormat(%q) = %q, want %q", tt.name, tt.args.text, got, tt.want)
|
||||||
}
|
|
||||||
|
|
||||||
if !utf8.ValidString(got2) {
|
|
||||||
t.Errorf("produced invalid UTF-8 string %q", got2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStripRaw(t *testing.T) {
|
func TestStripRaw(t *testing.T) {
|
||||||
for _, tt := range testsStripRaw {
|
type args struct {
|
||||||
if got := StripRaw(Fmt(tt.test)); got != tt.want {
|
text string
|
||||||
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) {
|
||||||
for _, tt := range testsValidNick {
|
type args struct {
|
||||||
if got := IsValidNick(tt.test); got != tt.want {
|
nick string
|
||||||
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) {
|
||||||
for _, tt := range testsValidChannel {
|
type args struct {
|
||||||
if got := IsValidChannel(tt.test); got != tt.want {
|
channel string
|
||||||
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) {
|
||||||
for _, tt := range testsValidUser {
|
type args struct {
|
||||||
if got := IsValidUser(tt.test); got != tt.want {
|
name string
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var testsToRFC1459 = []struct {
|
|
||||||
in string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"", ""},
|
|
||||||
{"a", "a"},
|
|
||||||
{"abcd", "abcd"},
|
|
||||||
{"AbcD", "abcd"},
|
|
||||||
{"!@#$%^&*()_+-=", "!@#$%~&*()_+-="},
|
|
||||||
{"Abcd[]", "abcd{}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzToRFC1459(f *testing.F) {
|
|
||||||
for _, tc := range testsToRFC1459 {
|
|
||||||
f.Add(tc.in)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
func TestToRFC1459(t *testing.T) {
|
||||||
for _, tt := range testsToRFC1459 {
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"a", "a"},
|
||||||
|
{"abcd", "abcd"},
|
||||||
|
{"AbcD", "abcd"},
|
||||||
|
{"!@#$%^&*()_+-=", "!@#$%~&*()_+-="},
|
||||||
|
{"Abcd[]", "abcd{}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range cases {
|
||||||
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) {
|
||||||
@ -398,37 +310,27 @@ func TestPatternWithoutGlobs(t *testing.T) {
|
|||||||
testGlobMatch(t, "test", "test")
|
testGlobMatch(t, "test", "test")
|
||||||
}
|
}
|
||||||
|
|
||||||
var testsGlob = []string{
|
func TestGlob(t *testing.T) {
|
||||||
"*test", // Leading.
|
cases := []string{
|
||||||
"this*", // Trailing.
|
"*test", // Leading.
|
||||||
"this*test", // Middle.
|
"this*", // Trailing.
|
||||||
"*is *", // String in between two.
|
"this*test", // Middle.
|
||||||
"*is*a*", // Lots.
|
"*is *", // String in between two.
|
||||||
"**test**", // Double glob characters.
|
"*is*a*", // Lots.
|
||||||
"**is**a***test*", // Varying number.
|
"**test**", // Double glob characters.
|
||||||
"* *", // White space between.
|
"**is**a***test*", // Varying number.
|
||||||
"*", // Lone.
|
"* *", // White space between.
|
||||||
"**********", // Nothing but globs.
|
"*", // Lone.
|
||||||
"*Ѿ*", // Unicode.
|
"**********", // Nothing but globs.
|
||||||
"*is a ϗѾ *", // Mixed ASCII/unicode.
|
"*Ѿ*", // Unicode.
|
||||||
}
|
"*is a ϗѾ *", // Mixed ASCII/unicode.
|
||||||
|
|
||||||
func FuzzGlob(f *testing.F) {
|
|
||||||
for _, tc := range testsGlob {
|
|
||||||
f.Add(tc, tc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, orig, orig2 string) {
|
for _, pattern := range cases {
|
||||||
_ = 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
10
go.mod
@ -1,9 +1,13 @@
|
|||||||
module github.com/yunginnanet/girc-atomic
|
module github.com/yunginnanet/girc-atomic
|
||||||
|
|
||||||
go 1.20
|
go 1.17
|
||||||
|
|
||||||
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/v2 v2.0.1
|
github.com/orcaman/concurrent-map v1.0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
12
go.sum
12
go.sum
@ -1,12 +1,11 @@
|
|||||||
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/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||||
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=
|
||||||
@ -15,5 +14,6 @@ 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=
|
||||||
|
71
handler.go
71
handler.go
@ -15,7 +15,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cmap "github.com/orcaman/concurrent-map/v2"
|
"github.com/orcaman/concurrent-map"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunHandlers manually runs handlers for a given event.
|
// RunHandlers manually runs handlers for a given event.
|
||||||
@ -25,18 +25,15 @@ func (c *Client) RunHandlers(event *Event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := strs.Get()
|
|
||||||
// Log the event.
|
// Log the event.
|
||||||
s.MustWriteString("< ")
|
prefix := "< "
|
||||||
if event.Echo {
|
if event.Echo {
|
||||||
s.MustWriteString("[echo-message] ")
|
prefix += "[echo-message] "
|
||||||
}
|
}
|
||||||
s.MustWriteString(event.String())
|
c.debug.Print(prefix + StripRaw(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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +78,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[string, cmap.ConcurrentMap[string, Handler]]
|
cm cmap.ConcurrentMap
|
||||||
}
|
}
|
||||||
|
|
||||||
type handlerTuple struct {
|
type handlerTuple struct {
|
||||||
@ -90,37 +87,40 @@ type handlerTuple struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newNestedHandlers() *nestedHandlers {
|
func newNestedHandlers() *nestedHandlers {
|
||||||
return &nestedHandlers{cm: cmap.New[cmap.ConcurrentMap[string, Handler]]()}
|
return &nestedHandlers{cm: cmap.New()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nest *nestedHandlers) len() (total int) {
|
func (nest *nestedHandlers) len() (total int) {
|
||||||
for hndlrs := range nest.cm.IterBuffered() {
|
for hs := range nest.cm.IterBuffered() {
|
||||||
total += len(hndlrs.Val.Keys())
|
hndlrs := hs.Val.(cmap.ConcurrentMap)
|
||||||
|
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)
|
||||||
hndlrs, ok := nest.cm.Get(cmd)
|
hs, ok := nest.cm.Get(cmd)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return hndlrs.Count()
|
hndlrs := hs.(cmap.ConcurrentMap)
|
||||||
|
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 cmap.ConcurrentMap[string, Handler]
|
var h interface{}
|
||||||
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 h.IterBuffered() {
|
for hi := range hm.IterBuffered() {
|
||||||
ht := handlerTuple{
|
ht := handlerTuple{
|
||||||
hi.Key,
|
hi.Key,
|
||||||
hi.Val,
|
hi.Val.(Handler),
|
||||||
}
|
}
|
||||||
handlers <- ht
|
handlers <- ht
|
||||||
}
|
}
|
||||||
@ -218,25 +218,35 @@ 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.
|
||||||
hmap, iok := c.internal.cm.Get(command)
|
ihm, 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
|
||||||
}
|
}
|
||||||
hndlr, _ := hmap.Get(cuid)
|
hi, _ := 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.
|
||||||
hmap, eok := c.external.cm.Get(command)
|
ehm, 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
|
||||||
}
|
}
|
||||||
hndlr, _ := hmap.Get(cuid)
|
hi, _ := 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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -324,12 +334,14 @@ 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 hs cmap.ConcurrentMap[string, Handler]
|
var h interface{}
|
||||||
hs, ok = c.external.cm.Get(cmd)
|
h, 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
|
||||||
@ -366,7 +378,8 @@ func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
parent *nestedHandlers
|
parent *nestedHandlers
|
||||||
chandlers cmap.ConcurrentMap[string, Handler]
|
chandlers cmap.ConcurrentMap
|
||||||
|
ei interface{}
|
||||||
ok bool
|
ok bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -376,10 +389,12 @@ func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid
|
|||||||
parent = c.external
|
parent = c.external
|
||||||
}
|
}
|
||||||
|
|
||||||
chandlers, ok = parent.cm.Get(cmd)
|
ei, ok = parent.cm.Get(cmd)
|
||||||
|
|
||||||
if !ok {
|
if ok {
|
||||||
chandlers = cmap.New[Handler]()
|
chandlers = ei.(cmap.ConcurrentMap)
|
||||||
|
} else {
|
||||||
|
chandlers = cmap.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
chandlers.Set(uid, handler)
|
chandlers.Set(uid, handler)
|
||||||
@ -534,8 +549,6 @@ 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())
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
68
modes.go
68
modes.go
@ -7,8 +7,7 @@ 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.
|
||||||
@ -119,14 +118,13 @@ 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.
|
||||||
// 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.
|
||||||
// B = Mode that changes a setting and always has a parameter.
|
// C = Mode that changes a setting and only has a parameter when set.
|
||||||
// C = Mode that changes a setting and only has a parameter when set.
|
// D = Mode that changes a setting and never has a parameter.
|
||||||
// D = Mode that changes a setting and never has a parameter.
|
// Note: Modes of type A return the list when there is no parameter present.
|
||||||
// Note: Modes of type A return the list when there is no parameter present.
|
// Note: Some clients assumes that any mode not listed is of type D.
|
||||||
// Note: 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.
|
||||||
// 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
|
||||||
@ -370,9 +368,13 @@ 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 && IsValidChannelMode(validmodes) {
|
if validmodes, ok := s.serverOptions.Get("CHANMODES"); ok {
|
||||||
return validmodes
|
modes := validmodes.(string)
|
||||||
|
if IsValidChannelMode(modes) {
|
||||||
|
return modes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ModeDefaults
|
return ModeDefaults
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,47 +382,63 @@ 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 prefix, ok := s.serverOptions.Get("PREFIX"); ok && isValidUserPrefix(prefix) {
|
if pi, ok := s.serverOptions.Get("PREFIX"); ok {
|
||||||
return prefix
|
prefix := pi.(string)
|
||||||
|
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 {
|
||||||
channels cmap.ConcurrentMap[string, *Perms]
|
mu sync.RWMutex
|
||||||
|
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: cmap.New[*Perms](),
|
channels: make(map[string]Perms),
|
||||||
}
|
}
|
||||||
for tuple := range p.channels.IterBuffered() {
|
for key := range p.channels {
|
||||||
np.channels.Set(tuple.Key, tuple.Val)
|
np.channels[key] = p.channels[key]
|
||||||
}
|
}
|
||||||
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) {
|
||||||
return p.channels.Get(ToRFC1459(channel))
|
p.mu.RLock()
|
||||||
|
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.channels.Set(ToRFC1459(channel), perms)
|
p.mu.Lock()
|
||||||
|
p.channels[ToRFC1459(channel)] = perms
|
||||||
|
p.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserPerms) remove(channel string) {
|
func (p *UserPerms) remove(channel string) {
|
||||||
p.channels.Remove(ToRFC1459(channel))
|
p.mu.Lock()
|
||||||
|
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
|
||||||
@ -446,7 +464,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
|
||||||
}
|
}
|
||||||
@ -456,7 +474,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
146
state.go
@ -10,7 +10,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cmap "github.com/orcaman/concurrent-map/v2"
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
)
|
)
|
||||||
|
|
||||||
// state represents the actively-changing variables within the client
|
// state represents the actively-changing variables within the client
|
||||||
@ -22,13 +22,12 @@ 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[string, *Channel]
|
channels cmap.ConcurrentMap
|
||||||
// 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[string, *User]
|
users cmap.ConcurrentMap
|
||||||
// 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.
|
||||||
@ -36,8 +35,7 @@ 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 map[string]string
|
serverOptions cmap.ConcurrentMap
|
||||||
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
|
||||||
@ -56,31 +54,22 @@ 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 = []Clearer{&s.channels, &s.users, &s.serverOptions}
|
var cmaps = []*cmap.ConcurrentMap{&s.channels, &s.users, &s.serverOptions}
|
||||||
for i, cm := range cmaps {
|
for _, cm := range cmaps {
|
||||||
switch {
|
if initial {
|
||||||
case i == 0 && initial:
|
*cm = cmap.New()
|
||||||
cm = cmap.New[*Channel]()
|
} else {
|
||||||
case i == 1 && initial:
|
|
||||||
cm = cmap.New[*User]()
|
|
||||||
case i == 2 && initial:
|
|
||||||
cm = cmap.New[string]()
|
|
||||||
default:
|
|
||||||
cm.Clear()
|
cm.Clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.enabledCap = cmap.New[map[string]string]()
|
s.enabledCap = make(map[string]map[string]string)
|
||||||
s.tmpCap = make(map[string]map[string]string)
|
s.tmpCap = make(map[string]map[string]string)
|
||||||
s.motd = ""
|
s.motd = ""
|
||||||
|
|
||||||
@ -92,11 +81,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 *MarshalableAtomicValue `json:"nick"`
|
Nick string `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 *MarshalableAtomicValue `json:"ident"`
|
Ident string `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
|
||||||
@ -104,7 +93,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 *MarshalableAtomicValue `json:"mask"`
|
Mask string `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.
|
||||||
@ -117,8 +106,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/v2/blob/893feb299719d9cbb2cfbe08b6dd4eb567d8039d/concurrent_map.go#L305
|
// https://github.com/orcaman/concurrent-map/blob/893feb299719d9cbb2cfbe08b6dd4eb567d8039d/concurrent_map.go#L305
|
||||||
ChannelList cmap.ConcurrentMap[string, *Channel] `json:"channels"`
|
ChannelList cmap.ConcurrentMap `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.
|
||||||
@ -154,8 +143,10 @@ type User struct {
|
|||||||
} `json:"extras"`
|
} `json:"extras"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channels returns a slice of pointers to Channel types that the client knows the user is in.
|
// Channels returns a reference of *Channels that the client knows the user
|
||||||
func (u *User) Channels(c *Client) []*Channel {
|
// is in. If you're just looking for the namme of the channels, use
|
||||||
|
// User.ChannelList.
|
||||||
|
func (u User) Channels(c *Client) []*Channel {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
panic("nil Client provided")
|
panic("nil Client provided")
|
||||||
}
|
}
|
||||||
@ -163,8 +154,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 := listed.Val
|
chn, chok := listed.Val.(*Channel)
|
||||||
if chn != nil {
|
if chok {
|
||||||
channels = append(channels, chn)
|
channels = append(channels, chn)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -189,9 +180,7 @@ func (u *User) Copy() *User {
|
|||||||
*nu = *u
|
*nu = *u
|
||||||
|
|
||||||
nu.Perms = u.Perms.Copy()
|
nu.Perms = u.Perms.Copy()
|
||||||
for ch := range u.ChannelList.IterBuffered() {
|
_ = copy(nu.ChannelList, u.ChannelList)
|
||||||
nu.ChannelList.Set(ch.Key, ch.Val)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nu
|
return nu
|
||||||
}
|
}
|
||||||
@ -210,7 +199,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.
|
||||||
@ -259,7 +248,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[string, *User] `json:"user_list"`
|
UserList cmap.ConcurrentMap `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"`
|
||||||
@ -271,7 +260,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")
|
||||||
}
|
}
|
||||||
@ -291,7 +280,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")
|
||||||
}
|
}
|
||||||
@ -316,7 +305,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")
|
||||||
}
|
}
|
||||||
@ -325,17 +314,19 @@ 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 ui == nil {
|
if !usrok {
|
||||||
if ui = c.state.lookupUser(listed.Key); ui == nil {
|
user = c.state.lookupUser(listed.Key)
|
||||||
|
if user == nil {
|
||||||
continue
|
continue
|
||||||
|
} else {
|
||||||
|
ch.UserList.Set(listed.Key, user)
|
||||||
}
|
}
|
||||||
ch.UserList.Set(listed.Key, ui)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
perms, ok := ui.Perms.Lookup(ch.Name)
|
perms, ok := user.Perms.Lookup(ch.Name)
|
||||||
if ok && perms.IsAdmin() {
|
if ok && perms.IsAdmin() {
|
||||||
users = append(users, ui)
|
users = append(users, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,9 +356,7 @@ func (ch *Channel) Copy() *Channel {
|
|||||||
nc := &Channel{}
|
nc := &Channel{}
|
||||||
*nc = *ch
|
*nc = *ch
|
||||||
|
|
||||||
for v := range ch.UserList.IterBuffered() {
|
_ = copy(nc.UserList, ch.UserList)
|
||||||
nc.UserList.Set(v.Val.Nick.Load().(string), v.Val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// And modes.
|
// And modes.
|
||||||
nc.Modes = ch.Modes.Copy()
|
nc.Modes = ch.Modes.Copy()
|
||||||
@ -404,7 +393,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[*User](),
|
UserList: cmap.New(),
|
||||||
Joined: time.Now(),
|
Joined: time.Now(),
|
||||||
Network: s.client.NetworkName(),
|
Network: s.client.NetworkName(),
|
||||||
Modes: NewCModes(supported, prefixes),
|
Modes: NewCModes(supported, prefixes),
|
||||||
@ -417,14 +406,17 @@ 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)
|
||||||
|
|
||||||
chn, ok := s.channels.Get(name)
|
c, 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() {
|
||||||
usr, uok := s.users.Get(listed.Key)
|
ui, _ := s.users.Get(listed.Key)
|
||||||
if uok {
|
usr, usrok := ui.(*User)
|
||||||
|
if usrok {
|
||||||
usr.deleteChannel(name)
|
usr.deleteChannel(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,55 +428,42 @@ 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))
|
||||||
if ci == nil || !cok {
|
chn, ok := ci.(*Channel)
|
||||||
|
if !ok || !cok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return ci
|
return chn
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
usr, uok := s.users.Get(ToRFC1459(name))
|
ui, uok := s.users.Get(ToRFC1459(name))
|
||||||
if usr == nil || !uok {
|
usr, ok := ui.(*User)
|
||||||
|
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 u, ok = s.users.Get(src.ID()); ok {
|
if _, ok := s.users.Get(src.ID()); ok {
|
||||||
// User already exists.
|
// User already exists.
|
||||||
return u, false
|
return nil, 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: NewAtomicString(src.Name),
|
Nick: src.Name,
|
||||||
Host: src.Host,
|
Host: src.Host,
|
||||||
Ident: NewAtomicString(src.Ident),
|
Ident: src.Ident,
|
||||||
Mask: NewAtomicString(mask.String()),
|
Mask: src.Name + "!" + src.Ident + "@" + src.Host,
|
||||||
ChannelList: cmap.New[*Channel](),
|
ChannelList: cmap.New(),
|
||||||
FirstSeen: time.Now(),
|
FirstSeen: time.Now(),
|
||||||
LastActive: time.Now(),
|
LastActive: time.Now(),
|
||||||
Network: s.client.NetworkName(),
|
Network: s.client.NetworkName(),
|
||||||
Perms: &UserPerms{channels: cmap.New[*Perms]()},
|
Perms: &UserPerms{channels: make(map[string]Perms)},
|
||||||
}
|
}
|
||||||
|
|
||||||
strs.MustPut(mask)
|
|
||||||
|
|
||||||
s.users.Set(src.ID(), u)
|
s.users.Set(src.ID(), u)
|
||||||
return u, true
|
return u, true
|
||||||
}
|
}
|
||||||
@ -535,19 +514,20 @@ func (s *state) renameUser(from, to string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if old != nil && user == nil {
|
if old != nil && user == nil {
|
||||||
user = old
|
user = old.(*User)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Nick.Store(to)
|
user.Nick = 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() {
|
||||||
chn := chanchan.Val
|
chi := chanchan.Val
|
||||||
if chn == nil {
|
chn, chok := chi.(*Channel)
|
||||||
|
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.Load().(string) != users[i] {
|
if fullUsers[i].Nick != 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)
|
||||||
var admList []string
|
admList := []string{}
|
||||||
for i := 0; i < len(adm); i++ {
|
for i := 0; i < len(adm); i++ {
|
||||||
admList = append(admList, adm[i].Nick.Load().(string))
|
admList = append(admList, adm[i].Nick)
|
||||||
}
|
}
|
||||||
trusted := ch.Trusted(c)
|
trusted := ch.Trusted(c)
|
||||||
var trustedList []string
|
trustedList := []string{}
|
||||||
for i := 0; i < len(trusted); i++ {
|
for i := 0; i < len(trusted); i++ {
|
||||||
trustedList = append(trustedList, trusted[i].Nick.Load().(string))
|
trustedList = append(trustedList, trusted[i].Nick)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Load().(string) != "fhjones" {
|
if user.Nick != "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.Load().(string) != "~user" {
|
if user.Ident != "~user" {
|
||||||
t.Errorf("User.Ident == %q, wanted \"~user\"", user.Ident)
|
t.Errorf("User.Ident == %q, wanted \"~user\"", user.Ident)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -250,9 +250,7 @@ func TestState(t *testing.T) {
|
|||||||
bounceStart <- true
|
bounceStart <- true
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err := conn.Write([]byte(mockConnStartState))
|
_, err := conn.Write([]byte(mockConnStartState))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -284,9 +282,10 @@ func TestState(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chn, chnok := user.ChannelList.Get("#channel")
|
chi, chnok := user.ChannelList.Get("#channel")
|
||||||
|
chn, chiok := chi.(*Channel)
|
||||||
|
|
||||||
if !chnok {
|
if !chnok || !chiok {
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -296,7 +295,8 @@ func TestState(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chn2, _ := user.ChannelList.Get("#channel2")
|
chi2, _ := 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,9 +316,7 @@ func TestState(t *testing.T) {
|
|||||||
bounceEnd <- true
|
bounceEnd <- true
|
||||||
})
|
})
|
||||||
|
|
||||||
if err = conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
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
Normal file
11
util.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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
29
value.go
@ -1,29 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user