Browse Source

migrate to github, add stuff.

master
Gisle Aune 2 years ago
parent
commit
bf1c50fbfa
  1. 2
      .drone.yml
  2. 2
      .idea/.gitignore
  3. 18
      .idea/dictionaries/gisle.xml
  4. 8
      .idea/irc.iml
  5. 6
      .idea/misc.xml
  6. 8
      .idea/modules.xml
  7. 6
      .idea/vcs.xml
  8. 29
      .idea/watcherTasks.xml
  9. 12
      channel.go
  10. 69
      client.go
  11. 231
      client_test.go
  12. 51
      cmd/ircrepl/main.go
  13. 5
      go.mod
  14. 13
      go.sum
  15. 6
      handle.go
  16. 4
      handle_test.go
  17. 29
      handlers/input.go
  18. 4
      handlers/mroleplay.go
  19. 2
      internal/irctest/assert.go
  20. 91
      internal/irctest/interaction.go
  21. 8
      internal/irctest/interaction_test.go
  22. 2
      ircutil/cut-message_test.go
  23. 78
      isupport/isupport.go
  24. 83
      isupport/isupport_test.go
  25. 19
      isupport/state.go
  26. 2
      list/list.go
  27. 4
      list/list_test.go
  28. 12
      query.go
  29. 24
      state.go
  30. 10
      status.go
  31. 1
      target.go

2
.drone.yml

@ -1,6 +1,6 @@
workspace:
base: /go
path: src/git.aiterp.net/gisle/irc
path: src/github.com/gissleh/irc
pipeline:
test:

2
.idea/.gitignore

@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

18
.idea/dictionaries/gisle.xml

@ -0,0 +1,18 @@
<component name="ProjectDictionaryState">
<dictionary name="gisle">
<words>
<w>chanmodes</w>
<w>chantypes</w>
<w>ctcp</w>
<w>fullnick</w>
<w>invalidcommand</w>
<w>isupport</w>
<w>polyfilled</w>
<w>privmsg</w>
<w>qcgimnprstz</w>
<w>structs</w>
<w>testserver</w>
<w>tstate</w>
</words>
</dictionary>
</component>

8
.idea/irc.iml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/irc.iml" filepath="$PROJECT_DIR$/.idea/irc.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

29
.idea/watcherTasks.xml

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="fmt $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="go" />
<option name="immediateSync" value="false" />
<option name="name" value="go fmt" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$GoExecPath$" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$ProjectFileDir$" />
<envs>
<env name="GOROOT" value="$GOROOT$" />
<env name="GOPATH" value="$GOPATH$" />
<env name="PATH" value="$GoBinDirs$" />
</envs>
</TaskOptions>
</component>
</project>

12
channel.go

@ -3,7 +3,7 @@ package irc
import (
"strings"
"git.aiterp.net/gisle/irc/list"
"github.com/gissleh/irc/list"
)
// A Channel is a target that manages the userlist
@ -23,6 +23,14 @@ func (channel *Channel) Name() string {
return channel.name
}
func (channel *Channel) State() TargetState {
return TargetState{
Kind: "channel",
Name: channel.name,
Users: channel.userlist.Users(),
}
}
// UserList gets the channel userlist
func (channel *Channel) UserList() list.Immutable {
return channel.userlist.Immutable()
@ -33,7 +41,7 @@ func (channel *Channel) Parted() bool {
return channel.parted
}
// Handle handles messages routed to this channel by the client's event loop
// AddHandler handles messages routed to this channel by the client's event loop
func (channel *Channel) Handle(event *Event, client *Client) {
switch event.Name() {
case "packet.join":

69
client.go

@ -11,14 +11,15 @@ import (
"fmt"
mathRand "math/rand"
"net"
"sort"
"strconv"
"strings"
"sync"
"time"
"git.aiterp.net/gisle/irc/ircutil"
"git.aiterp.net/gisle/irc/isupport"
"git.aiterp.net/gisle/irc/list"
"github.com/gissleh/irc/ircutil"
"github.com/gissleh/irc/isupport"
"github.com/gissleh/irc/list"
)
var supportedCaps = []string{
@ -31,6 +32,7 @@ var supportedCaps = []string{
"extended-join",
"chghost",
"account-tag",
"echo-message",
}
// ErrNoConnection is returned if you try to do something requiring a connection,
@ -82,7 +84,7 @@ type Client struct {
status *Status
targets []Target
targteIds map[Target]string
targetIds map[Target]string
}
// New creates a new client. The context can be context.Background if you want manually to
@ -96,7 +98,7 @@ func New(ctx context.Context, config Config) *Client {
capEnabled: make(map[string]bool),
capData: make(map[string]string),
config: config.WithDefaults(),
targteIds: make(map[Target]string, 16),
targetIds: make(map[Target]string, 16),
status: &Status{},
}
@ -170,6 +172,39 @@ func (client *Client) Ready() bool {
return client.ready
}
func (client *Client) State() ClientState {
client.mutex.RLock()
state := ClientState{
Nick: client.nick,
User: client.user,
Host: client.host,
Connected: client.conn != nil,
Ready: client.ready,
ISupport: client.isupport.State(),
Caps: make([]string, 0, len(client.capEnabled)),
Targets: make([]TargetState, 0, len(client.targets)),
}
for key, enabled := range client.capEnabled {
if enabled {
state.Caps = append(state.Caps, key)
}
}
for _, target := range client.targets {
tstate := target.State()
tstate.ID = client.targetIds[target]
state.Targets = append(state.Targets, tstate)
}
client.mutex.RUnlock()
sort.Strings(state.Caps)
return state
}
// Connect connects to the server by addr.
func (client *Client) Connect(addr string, ssl bool) (err error) {
var conn net.Conn
@ -411,12 +446,12 @@ func (client *Client) EmitInput(line string, target Target) context.Context {
if target != nil {
client.mutex.RLock()
event.targets = append(event.targets, target)
event.targetIds[target] = client.targteIds[target]
event.targetIds[target] = client.targetIds[target]
client.mutex.RUnlock()
} else {
client.mutex.RLock()
event.targets = append(event.targets, client.status)
event.targetIds[client.status] = client.targteIds[client.status]
event.targetIds[client.status] = client.targetIds[client.status]
client.mutex.RUnlock()
}
@ -576,7 +611,7 @@ func (client *Client) AddTarget(target Target) (id string, err error) {
id = generateClientID()
client.targets = append(client.targets, target)
client.targteIds[target] = id
client.targetIds[target] = id
return
}
@ -592,11 +627,11 @@ func (client *Client) RemoveTarget(target Target) (id string, err error) {
for i := range client.targets {
if target == client.targets[i] {
id = client.targteIds[target]
id = client.targetIds[target]
client.targets[i] = client.targets[len(client.targets)-1]
client.targets = client.targets[:len(client.targets)-1]
delete(client.targteIds, target)
delete(client.targetIds, target)
// Ensure the channel has been parted
if channel, ok := target.(*Channel); ok && !channel.parted {
@ -828,7 +863,7 @@ func (client *Client) handleEvent(event *Event) {
}
}
// Handle ISupport
// AddHandler ISupport
case "packet.005":
{
for _, token := range event.Args[1:] {
@ -1002,6 +1037,7 @@ func (client *Client) handleEvent(event *Event) {
}
if event.Nick == client.nick {
channel.parted = true
client.RemoveTarget(channel)
} else {
client.handleInTarget(channel, event)
@ -1016,6 +1052,7 @@ func (client *Client) handleEvent(event *Event) {
}
if event.Arg(1) == client.nick {
channel.parted = true
client.RemoveTarget(channel)
} else {
client.handleInTarget(channel, event)
@ -1159,7 +1196,7 @@ func (client *Client) handleEvent(event *Event) {
channels = append(channels, channel.Name())
rejoinEvent.targets = append(rejoinEvent.targets, target)
rejoinEvent.targetIds[target] = client.targteIds[target]
rejoinEvent.targetIds[target] = client.targetIds[target]
}
}
client.mutex.RUnlock()
@ -1197,7 +1234,7 @@ func (client *Client) handleInTargets(nick string, event *Event) {
target.Handle(event, client)
event.targets = append(event.targets, target)
event.targetIds[target] = client.targteIds[target]
event.targetIds[target] = client.targetIds[target]
}
case *Query:
{
@ -1205,7 +1242,7 @@ func (client *Client) handleInTargets(nick string, event *Event) {
target.Handle(event, client)
event.targets = append(event.targets, target)
event.targetIds[target] = client.targteIds[target]
event.targetIds[target] = client.targetIds[target]
}
}
case *Status:
@ -1214,7 +1251,7 @@ func (client *Client) handleInTargets(nick string, event *Event) {
target.Handle(event, client)
event.targets = append(event.targets, target)
event.targetIds[target] = client.targteIds[target]
event.targetIds[target] = client.targetIds[target]
}
}
}
@ -1233,7 +1270,7 @@ func (client *Client) handleInTarget(target Target, event *Event) {
target.Handle(event, client)
event.targets = append(event.targets, target)
event.targetIds[target] = client.targteIds[target]
event.targetIds[target] = client.targetIds[target]
client.mutex.RUnlock()
}

231
client_test.go

@ -5,15 +5,15 @@ import (
"errors"
"testing"
"git.aiterp.net/gisle/irc"
"git.aiterp.net/gisle/irc/handlers"
"git.aiterp.net/gisle/irc/internal/irctest"
"github.com/gissleh/irc"
"github.com/gissleh/irc/handlers"
"github.com/gissleh/irc/internal/irctest"
)
// Integration test below, brace yourself.
func TestClient(t *testing.T) {
irc.Handle(handlers.Input)
irc.Handle(handlers.MRoleplay)
irc.AddHandler(handlers.Input)
irc.AddHandler(handlers.MRoleplay)
client := irc.New(context.Background(), irc.Config{
Nick: "Test",
@ -31,13 +31,13 @@ func TestClient(t *testing.T) {
interaction := irctest.Interaction{
Strict: false,
Lines: []irctest.InteractionLine{
{Kind: 'C', Data: "CAP LS 302"},
{Kind: 'C', Data: "NICK Test"},
{Kind: 'C', Data: "USER Tester 8 * :..."},
{Kind: 'S', Data: ":testserver.example.com CAP * LS :multi-prefix chghost userhost-in-names vendorname/custom-stuff echo-message =malformed vendorname/advanced-custom-stuff=things,and,items"},
{Kind: 'C', Data: "CAP REQ :multi-prefix chghost userhost-in-names"},
{Kind: 'S', Data: ":testserver.example.com CAP * ACK :multi-prefix userhost-in-names"},
{Kind: 'C', Data: "CAP END"},
{Client: "CAP LS 302"},
{Client: "NICK Test"},
{Client: "USER Tester 8 * :..."},
{Server: ":testserver.example.com CAP * LS :multi-prefix chghost userhost-in-names vendorname/custom-stuff echo-message =malformed vendorname/advanced-custom-stuff=things,and,items"},
{Client: "CAP REQ :multi-prefix chghost userhost-in-names"},
{Server: ":testserver.example.com CAP * ACK :multi-prefix userhost-in-names"},
{Client: "CAP END"},
{Callback: func() error {
if !client.CapEnabled("multi-prefix") {
return errors.New("multi-prefix cap should be enabled.")
@ -54,39 +54,39 @@ func TestClient(t *testing.T) {
return nil
}},
{Kind: 'S', Data: ":testserver.example.com 433 * Test :Nick is not available"},
{Kind: 'C', Data: "NICK Test2"},
{Kind: 'S', Data: ":testserver.example.com 433 * Test2 :Nick is not available"},
{Kind: 'C', Data: "NICK Test3"},
{Kind: 'S', Data: ":testserver.example.com 433 * Test3 :Nick is not available"},
{Kind: 'C', Data: "NICK Test4"},
{Kind: 'S', Data: ":testserver.example.com 433 * Test4 :Nick is not available"},
{Kind: 'C', Data: "NICK Test768"},
{Kind: 'S', Data: ":testserver.example.com 001 Test768 :Welcome to the TestServer Internet Relay Chat Network test"},
{Kind: 'C', Data: "WHO Test768*"},
{Kind: 'S', Data: ":testserver.example.com 002 Test768 :Your host is testserver.example.com[testserver.example.com/6667], running version charybdis-4-rc3"},
{Kind: 'S', Data: ":testserver.example.com 003 Test768 :This server was created Fri Nov 25 2016 at 17:28:20 CET"},
{Kind: 'S', Data: ":testserver.example.com 004 Test768 testserver.example.com charybdis-4-rc3 DQRSZagiloswxz CFILNPQbcefgijklmnopqrstvz bkloveqjfI"},
{Kind: 'S', Data: ":testserver.example.com 005 Test768 FNC SAFELIST ELIST=CTU MONITOR=100 WHOX ETRACE KNOCK CHANTYPES=#& EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLNPQcgimnprstz CHANLIMIT=#&:15 :are supported by this server"},
{Kind: 'S', Data: ":testserver.example.com 005 Test768 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=TestServer STATUSMSG=@+ CALLERID=g CASEMAPPING=rfc1459 NICKLEN=30 MAXNICKLEN=31 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server"},
{Kind: 'S', Data: ":testserver.example.com 005 Test768 TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,&acjmorsuxz| CLIENTVER=3.0 :are supported by this server"},
{Kind: 'S', Data: ":testserver.example.com 251 Test768 :There are 0 users and 2 invisible on 1 servers"},
{Kind: 'S', Data: ":testserver.example.com 254 Test768 1 :channels formed"},
{Kind: 'S', Data: ":testserver.example.com 255 Test768 :I have 2 clients and 0 servers"},
{Kind: 'S', Data: ":testserver.example.com 265 Test768 2 2 :Current local users 2, max 2"},
{Kind: 'S', Data: ":testserver.example.com 266 Test768 2 2 :Current global users 2, max 2"},
{Kind: 'S', Data: ":testserver.example.com 250 Test768 :Highest connection count: 2 (2 clients) (8 connections received)"},
{Kind: 'S', Data: ":testserver.example.com 375 Test768 :- testserver.example.com Message of the Day - "},
{Kind: 'S', Data: ":testserver.example.com 372 Test768 :- This server is only for testing irce, not chatting. If you happen"},
{Kind: 'S', Data: ":testserver.example.com 372 Test768 :- to connect to it by accident, please disconnect immediately."},
{Kind: 'S', Data: ":testserver.example.com 372 Test768 :- "},
{Kind: 'S', Data: ":testserver.example.com 372 Test768 :- - #Test :: Test Channel"},
{Kind: 'S', Data: ":testserver.example.com 372 Test768 :- - #Test2 :: Other Test Channel"},
{Kind: 'S', Data: ":testserver.example.com 376 Test768 :End of /MOTD command."},
{Kind: 'S', Data: ":testserver.example.com 352 Test768 * ~Tester testclient.example.com testserver.example.com Test768 H :0 ..."},
{Kind: 'S', Data: ":Test768 MODE Test768 :+i"},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Server: ":testserver.example.com 433 * Test :Nick is not available"},
{Client: "NICK Test2"},
{Server: ":testserver.example.com 433 * Test2 :Nick is not available"},
{Client: "NICK Test3"},
{Server: ":testserver.example.com 433 * Test3 :Nick is not available"},
{Client: "NICK Test4"},
{Server: ":testserver.example.com 433 * Test4 :Nick is not available"},
{Client: "NICK Test768"},
{Server: ":testserver.example.com 001 Test768 :Welcome to the TestServer Internet Relay Chat Network test"},
{Client: "WHO Test768*"},
{Server: ":testserver.example.com 002 Test768 :Your host is testserver.example.com[testserver.example.com/6667], running version charybdis-4-rc3"},
{Server: ":testserver.example.com 003 Test768 :This server was created Fri Nov 25 2016 at 17:28:20 CET"},
{Server: ":testserver.example.com 004 Test768 testserver.example.com charybdis-4-rc3 DQRSZagiloswxz CFILNPQbcefgijklmnopqrstvz bkloveqjfI"},
{Server: ":testserver.example.com 005 Test768 FNC SAFELIST ELIST=CTU MONITOR=100 WHOX ETRACE KNOCK CHANTYPES=#& EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLNPQcgimnprstz CHANLIMIT=#&:15 :are supported by this server"},
{Server: ":testserver.example.com 005 Test768 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=TestServer STATUSMSG=@+ CALLERID=g CASEMAPPING=rfc1459 NICKLEN=30 MAXNICKLEN=31 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server"},
{Server: ":testserver.example.com 005 Test768 TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,&acjmorsuxz| CLIENTVER=3.0 :are supported by this server"},
{Server: ":testserver.example.com 251 Test768 :There are 0 users and 2 invisible on 1 servers"},
{Server: ":testserver.example.com 254 Test768 1 :channels formed"},
{Server: ":testserver.example.com 255 Test768 :I have 2 clients and 0 servers"},
{Server: ":testserver.example.com 265 Test768 2 2 :Current local users 2, max 2"},
{Server: ":testserver.example.com 266 Test768 2 2 :Current global users 2, max 2"},
{Server: ":testserver.example.com 250 Test768 :Highest connection count: 2 (2 clients) (8 connections received)"},
{Server: ":testserver.example.com 375 Test768 :- testserver.example.com Message of the Day - "},
{Server: ":testserver.example.com 372 Test768 :- This server is only for testing irce, not chatting. If you happen"},
{Server: ":testserver.example.com 372 Test768 :- to connect to it by accident, please disconnect immediately."},
{Server: ":testserver.example.com 372 Test768 :- "},
{Server: ":testserver.example.com 372 Test768 :- - #Test :: Test Channel"},
{Server: ":testserver.example.com 372 Test768 :- - #Test2 :: Other Test Channel"},
{Server: ":testserver.example.com 376 Test768 :End of /MOTD command."},
{Server: ":testserver.example.com 352 Test768 * ~Tester testclient.example.com testserver.example.com Test768 H :0 ..."},
{Server: ":Test768 MODE Test768 :+i"},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
if client.Nick() != "Test768" {
return errors.New("client.Nick shouldn't be " + client.Nick())
@ -104,12 +104,12 @@ func TestClient(t *testing.T) {
client.Join("#Test")
return nil
}},
{Kind: 'C', Data: "JOIN #Test"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 JOIN #Test *"},
{Kind: 'S', Data: ":testserver.example.com 353 Test768 = #Test :Test768!~Tester@127.0.0.1 @+Gisle!irce@10.32.0.1"},
{Kind: 'S', Data: ":testserver.example.com 366 Test768 #Test :End of /NAMES list."},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Client: "JOIN #Test"},
{Server: ":Test768!~Tester@127.0.0.1 JOIN #Test *"},
{Server: ":testserver.example.com 353 Test768 = #Test :Test768!~Tester@127.0.0.1 @+Gisle!irce@10.32.0.1"},
{Server: ":testserver.example.com 366 Test768 #Test :End of /NAMES list."},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
if client.Channel("#Test") == nil {
return errors.New("Channel #Test not found")
@ -117,13 +117,13 @@ func TestClient(t *testing.T) {
return nil
}},
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +osv Test768 Test768"},
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +N-s "},
{Kind: 'S', Data: ":Test1234!~test2@172.17.37.1 JOIN #Test Test1234"},
{Kind: 'S', Data: ":Test4321!~test2@172.17.37.1 JOIN #Test Test1234"},
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +v Test1234"},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Server: ":Gisle!~irce@10.32.0.1 MODE #Test +osv Test768 Test768"},
{Server: ":Gisle!~irce@10.32.0.1 MODE #Test +N-s "},
{Server: ":Test1234!~test2@172.17.37.1 JOIN #Test Test1234"},
{Server: ":Test4321!~test2@172.17.37.1 JOIN #Test Test1234"},
{Server: ":Gisle!~irce@10.32.0.1 MODE #Test +v Test1234"},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
channel := client.Channel("#Test")
if channel == nil {
@ -145,14 +145,14 @@ func TestClient(t *testing.T) {
return nil
}},
{Kind: 'S', Data: ":Test1234!~test2@172.17.37.1 NICK Hunter2"},
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 AWAY :Doing stuff"},
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 AWAY"},
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 PART #Test :Leaving the channel"},
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 CHGHOST test2 some.awesome.virtual.host"},
{Kind: 'S', Data: "@account=Hunter2 :Test4321!~test2@172.17.37.1 PRIVMSG #Test :Hello World."},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Server: ":Test1234!~test2@172.17.37.1 NICK Hunter2"},
{Server: ":Hunter2!~test2@172.17.37.1 AWAY :Doing stuff"},
{Server: ":Gisle!~irce@10.32.0.1 AWAY"},
{Server: ":Gisle!~irce@10.32.0.1 PART #Test :Leaving the channel"},
{Server: ":Hunter2!~test2@172.17.37.1 CHGHOST test2 some.awesome.virtual.host"},
{Server: "@account=Hunter2 :Test4321!~test2@172.17.37.1 PRIVMSG #Test :Hello World."},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
channel := client.Channel("#Test")
if channel == nil {
@ -188,9 +188,9 @@ func TestClient(t *testing.T) {
return nil
}},
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 PRIVMSG Test768 :Hello, World"},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Server: ":Hunter2!~test2@172.17.37.1 PRIVMSG Test768 :Hello, World"},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
query := client.Query("Hunter2")
if query == nil {
@ -199,12 +199,12 @@ func TestClient(t *testing.T) {
return nil
}},
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 NICK SevenAsterisks"},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Server: ":Hunter2!~test2@172.17.37.1 NICK SevenAsterisks"},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
oldQuerry := client.Query("Hunter2")
if oldQuerry != nil {
oldQuery := client.Query("Hunter2")
if oldQuery != nil {
return errors.New("Did find query by old name")
}
@ -219,8 +219,8 @@ func TestClient(t *testing.T) {
client.EmitInput("/invalidcommand stuff and things", nil)
return nil
}},
{Kind: 'C', Data: "INVALIDCOMMAND stuff and things"},
{Kind: 'S', Data: ":testserver.example.com 421 Test768 INVALIDCOMMAND :Unknown command"},
{Client: "INVALIDCOMMAND stuff and things"},
{Server: ":testserver.example.com 421 Test768 INVALIDCOMMAND :Unknown command"},
{Callback: func() error {
channel := client.Channel("#Test")
if channel == nil {
@ -233,14 +233,14 @@ func TestClient(t *testing.T) {
client.EmitInput("Hello again", channel)
return nil
}},
{Kind: 'C', Data: "PRIVMSG #Test :\x01ACTION does stuff\x01"},
{Kind: 'C', Data: "PRIVMSG #Test :\x01ACTION describes stuff\x01"},
{Kind: 'C', Data: "PRIVMSG #Test :Hello, World"},
{Kind: 'C', Data: "PRIVMSG #Test :Hello again"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :\x01ACTION does stuff\x01"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :\x01ACTION describes stuff\x01"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :Hello, World"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :Hello again"},
{Client: "PRIVMSG #Test :\x01ACTION does stuff\x01"},
{Client: "PRIVMSG #Test :\x01ACTION describes stuff\x01"},
{Client: "PRIVMSG #Test :Hello, World"},
{Client: "PRIVMSG #Test :Hello again"},
{Server: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :\x01ACTION does stuff\x01"},
{Server: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :\x01ACTION describes stuff\x01"},
{Server: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :Hello, World"},
{Server: ":Test768!~Tester@127.0.0.1 PRIVMSG #Test :Hello again"},
{Callback: func() error {
channel := client.Channel("#Test")
if channel == nil {
@ -251,10 +251,10 @@ func TestClient(t *testing.T) {
client.EmitInput("/npcac Test_NPC stuffs things", channel)
return nil
}},
{Kind: 'C', Data: "MODE #Test +N"},
{Kind: 'C', Data: "NPCA #Test Test_NPC :stuffs things"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 MODE #Test +N"},
{Kind: 'S', Data: ":\x1FTest_NPC\x1F!Test768@npc.fakeuser.invalid PRIVMSG #Test :\x01ACTION stuffs things\x01"},
{Client: "MODE #Test +N"},
{Client: "NPCA #Test Test_NPC :stuffs things"},
{Server: ":Test768!~Tester@127.0.0.1 MODE #Test +N"},
{Server: ":\x1FTest_NPC\x1F!Test768@npc.fakeuser.invalid PRIVMSG #Test :\x01ACTION stuffs things\x01"},
{Callback: func() error {
channel := client.Channel("#Test")
if channel == nil {
@ -265,16 +265,21 @@ func TestClient(t *testing.T) {
client.Sayf(channel.Name(), "Hello, %s", "World")
return nil
}},
{Kind: 'C', Data: "PRIVMSG #Test :\x01ACTION does stuff with 42 things\x01"},
{Kind: 'C', Data: "PRIVMSG #Test :Hello, World"},
{Client: "PRIVMSG #Test :\x01ACTION does stuff with 42 things\x01"},
{Client: "PRIVMSG #Test :Hello, World"},
{Callback: func() error {
client.Part("#Test")
return nil
channel := client.Channel("#Test")
if channel == nil {
return errors.New("#Test doesn't exist")
}
_, err := client.RemoveTarget(channel)
return err
}},
{Kind: 'C', Data: "PART #Test"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 PART #Test"},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Client: "PART #Test"},
{Server: ":Test768!~Tester@127.0.0.1 PART #Test"},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
if client.Channel("#Test") != nil {
return errors.New("#Test is still there.")
@ -286,12 +291,12 @@ func TestClient(t *testing.T) {
client.Join("#Test2")
return nil
}},
{Kind: 'C', Data: "JOIN #Test2"},
{Kind: 'S', Data: ":Test768!~Tester@127.0.0.1 JOIN #Test2 *"},
{Kind: 'S', Data: ":testserver.example.com 353 Test768 = #Test2 :Test768!~Tester@127.0.0.1 +DoomedUser!doom@example.com @+ZealousMod!zeal@example.com"},
{Kind: 'S', Data: ":testserver.example.com 366 Test768 #Test2 :End of /NAMES list."},
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com"},
{Client: "JOIN #Test2"},
{Server: ":Test768!~Tester@127.0.0.1 JOIN #Test2 *"},
{Server: ":testserver.example.com 353 Test768 = #Test2 :Test768!~Tester@127.0.0.1 +DoomedUser!doom@example.com @+ZealousMod!zeal@example.com"},
{Server: ":testserver.example.com 366 Test768 #Test2 :End of /NAMES list."},
{Server: "PING :testserver.example.com"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com"},
{Callback: func() error {
channel := client.Channel("#Test2")
if channel == nil {
@ -300,9 +305,9 @@ func TestClient(t *testing.T) {
return irctest.AssertUserlist(t, channel, "@ZealousMod", "+DoomedUser", "Test768")
}},
{Kind: 'S', Data: ":ZealousMod!zeal@example.com KICK #Test2 DoomedUser :Kickety kick"},
{Kind: 'S', Data: "PING :testserver.example.com sync"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com sync"},
{Server: ":ZealousMod!zeal@example.com KICK #Test2 DoomedUser :Kickety kick"},
{Server: "PING :testserver.example.com sync"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com sync"},
{Callback: func() error {
channel := client.Channel("#Test2")
if channel == nil {
@ -311,9 +316,9 @@ func TestClient(t *testing.T) {
return irctest.AssertUserlist(t, channel, "@ZealousMod", "Test768")
}},
{Kind: 'S', Data: ":ZealousMod!zeal@example.com KICK #Test2 Test768 :Kickety kick"},
{Kind: 'S', Data: "PING :testserver.example.com sync"}, // Ping/Pong to sync.
{Kind: 'C', Data: "PONG :testserver.example.com sync"},
{Server: ":ZealousMod!zeal@example.com KICK #Test2 Test768 :Kickety kick"},
{Server: "PING :testserver.example.com sync"}, // Ping/Pong to sync.
{Client: "PONG :testserver.example.com sync"},
{Callback: func() error {
if client.Channel("#Test2") != nil {
return errors.New("#Test2 is still there.")
@ -348,8 +353,12 @@ func TestClient(t *testing.T) {
t.Error("CBErr:", fail.CBErr)
t.Error("Result:", fail.Result)
if fail.Index >= 0 {
t.Error("Line.Kind:", interaction.Lines[fail.Index].Kind)
t.Error("Line.Data:", interaction.Lines[fail.Index].Data)
if interaction.Lines[fail.Index].Server != "" {
t.Error("Line.Server:", interaction.Lines[fail.Index].Server)
}
if interaction.Lines[fail.Index].Client != "" {
t.Error("Line.Client:", interaction.Lines[fail.Index].Client)
}
}
}
@ -368,7 +377,7 @@ func TestParenthesesBug(t *testing.T) {
Nick: "Stuff",
})
irc.Handle(func(event *irc.Event, client *irc.Client) {
irc.AddHandler(func(event *irc.Event, client *irc.Client) {
if event.Name() == "packet.privmsg" || event.Nick == "Dante" {
gotMessage = true

51
cmd/ircrepl/main.go

@ -6,10 +6,12 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/gissleh/irc/handlers"
"log"
"os"
"strings"
"git.aiterp.net/gisle/irc"
"github.com/gissleh/irc"
)
var flagNick = flag.String("nick", "Test", "The client nick")
@ -25,6 +27,9 @@ func main() {
flag.Parse()
irc.AddHandler(handlers.Input)
irc.AddHandler(handlers.MRoleplay)
client := irc.New(ctx, irc.Config{
Nick: *flagNick,
User: *flagUser,
@ -37,13 +42,49 @@ func main() {
fmt.Fprintf(os.Stderr, "Failed to connect: %s", err)
}
irc.Handle(func(event *irc.Event, client *irc.Client) {
json, err := json.MarshalIndent(event, "", " ")
var target irc.Target
irc.AddHandler(func(event *irc.Event, client *irc.Client) {
if event.Name() == "input.target" {
name := event.Arg(0)
if client.ISupport().IsChannel(name) {
log.Println("Set target channel", name)
target = client.Channel(name)
} else if len(name) > 0 {
log.Println("Set target query", name)
target = client.Query(name)
} else {
log.Println("Set target status")
target = client.Status()
}
if target == nil {
log.Println("Target does not exist, set to status")
target = client.Status()
}
event.PreventDefault()
return
}
if event.Name() == "input.clientstatus" {
j, err := json.MarshalIndent(client.State(), "", " ")
if err != nil {
return
}
fmt.Println(string(j))
event.PreventDefault()
return
}
j, err := json.MarshalIndent(event, "", " ")
if err != nil {
return
}
fmt.Println(string(json))
fmt.Println(string(j))
})
reader := bufio.NewReader(os.Stdin)
@ -53,6 +94,6 @@ func main() {
break
}
client.EmitInput(string(line[:len(line)-1]), client.Status())
client.EmitInput(string(line[:len(line)-1]), target)
}
}

5
go.mod

@ -0,0 +1,5 @@
module github.com/gissleh/irc
go 1.12
require github.com/stretchr/testify v1.4.0

13
go.sum

@ -0,0 +1,13 @@
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/gissleh/irc v0.0.0-20190420194256-9a9cd33fd83c h1:SK61/nAM56ylhrhYi1289yOBpsupGEOQTZuFYxDNhrw=
github.com/gissleh/irc v0.0.0-20190420194256-9a9cd33fd83c/go.mod h1:7uklwlzI0XI2O8ttooTH0suUpUmQ8Pviplg36smc1Aw=
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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

6
handle.go

@ -14,12 +14,12 @@ func emit(event *Event, client *Client) {
}
}
// Handle adds a new handler to the irc handling. The handler may be called from multiple threads at the same
// AddHandler adds a new handler to the irc handling. The handler may be called from multiple threads at the same
// time, so external resources should be locked if there are multiple clients. Adding handlers is not thread
// safe and should be done prior to clients being created.Handle. Also, this handler will block the individual
// safe and should be done prior to clients being created.AddHandler. Also, this handler will block the individual
// client's event loop, so long operations that include network requests and the like should be done in a
// goroutine with the needed data **copied** from the handler function.
func Handle(handler Handler) {
func AddHandler(handler Handler) {
eventHandler.handlers = append(eventHandler.handlers, handler)
}

4
handle_test.go

@ -7,7 +7,7 @@ import (
"testing"
"time"
"git.aiterp.net/gisle/irc"
"github.com/gissleh/irc"
)
func TestHandle(t *testing.T) {
@ -18,7 +18,7 @@ func TestHandle(t *testing.T) {
event := irc.NewEvent("test", eventName)
handled := false
irc.Handle(func(event *irc.Event, client *irc.Client) {
irc.AddHandler(func(event *irc.Event, client *irc.Client) {
t.Log("Got:", event.Kind(), event.Verb())
if event.Kind() == "test" && event.Verb() == eventName {

29
handlers/input.go

@ -1,8 +1,9 @@
package handlers
import (
"git.aiterp.net/gisle/irc"
"git.aiterp.net/gisle/irc/ircutil"
"github.com/gissleh/irc"
"github.com/gissleh/irc/ircutil"
"time"
)
// Input handles the default input.
@ -70,6 +71,18 @@ func Input(event *irc.Event, client *irc.Client) {
cuts := ircutil.CutMessage(event.Text, overhead)
for _, cut := range cuts {
client.SendCTCP("ACTION", target.Name(), false, cut)
if !client.CapEnabled("echo-message") {
event := irc.NewEvent("echo", "action")
event.Time = time.Now()
event.Nick = client.Nick()
event.User = client.User()
event.Host = client.Host()
event.Args = []string{target.Name()}
event.Text = cut
client.EmitNonBlocking(event)
}
}
}
@ -88,6 +101,18 @@ func Input(event *irc.Event, client *irc.Client) {
cuts := ircutil.CutMessage(text, overhead)
for _, cut := range cuts {
client.SendCTCP("ACTION", targetName, false, cut)
if !client.CapEnabled("echo-message") {
event := irc.NewEvent("echo", "action")
event.Time = time.Now()
event.Nick = client.Nick()
event.User = client.User()
event.Host = client.Host()
event.Args = []string{targetName}
event.Text = cut
client.EmitNonBlocking(event)
}
}
}

4
handlers/mroleplay.go

@ -3,8 +3,8 @@ package handlers
import (
"strings"
"git.aiterp.net/gisle/irc"
"git.aiterp.net/gisle/irc/ircutil"
"github.com/gissleh/irc"
"github.com/gissleh/irc/ircutil"
)
// MRoleplay is a handler that adds commands for cutting NPC commands, as well as cleaning up

2
internal/irctest/assert.go

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"git.aiterp.net/gisle/irc"
"github.com/gissleh/irc"
)
// AssertUserlist compares the userlist to a list of prefixed nicks

91
internal/irctest/interaction.go

@ -49,58 +49,49 @@ func (interaction *Interaction) Listen() (addr string, err error) {
for i := 0; i < len(lines); i++ {
line := lines[i]
if line.Data != "" {
switch line.Kind {
case 'S':
{
_, err := conn.Write(append([]byte(line.Data), '\r', '\n'))
if err != nil {
interaction.Failure = &InteractionFailure{
Index: i, NetErr: err,
}
return
}
if line.Server != "" {
_, err := conn.Write(append([]byte(line.Server), '\r', '\n'))
if err != nil {
interaction.Failure = &InteractionFailure{
Index: i, NetErr: err,
}
case 'C':
{
conn.SetReadDeadline(time.Now().Add(time.Second * 2))
input, err := reader.ReadString('\n')
if err != nil {
interaction.Failure = &InteractionFailure{
Index: i, NetErr: err,
}
return
}
input = strings.Replace(input, "\r", "", -1)
input = strings.Replace(input, "\n", "", 1)
match := line.Data
success := false
if strings.HasSuffix(match, "*") {
success = strings.HasPrefix(input, match[:len(match)-1])
} else {
success = match == input
}
interaction.Log = append(interaction.Log, input)
if !success {
if !interaction.Strict {
i--
continue
}
interaction.Failure = &InteractionFailure{
Index: i, Result: input,
}
return
}
return
}
} else if line.Client != "" {
conn.SetReadDeadline(time.Now().Add(time.Second * 2))
input, err := reader.ReadString('\n')
if err != nil {
interaction.Failure = &InteractionFailure{
Index: i, NetErr: err,
}
return
}
}
input = strings.Replace(input, "\r", "", -1)
input = strings.Replace(input, "\n", "", 1)
match := line.Client
success := false
if line.Callback != nil {
if strings.HasSuffix(match, "*") {
success = strings.HasPrefix(input, match[:len(match)-1])
} else {
success = match == input
}
interaction.Log = append(interaction.Log, input)
if !success {
if !interaction.Strict {
i--
continue
}
interaction.Failure = &InteractionFailure{
Index: i, Result: input,
}
return
}
} else if line.Callback != nil {
err := line.Callback()
if err != nil {
interaction.Failure = &InteractionFailure{
@ -132,7 +123,7 @@ type InteractionFailure struct {
// InteractionLine is part of an interaction, whether it is a line
// that is sent to a client or a line expected from a client.
type InteractionLine struct {
Kind byte
Data string
Client string
Server string
Callback func() error
}

8
internal/irctest/interaction_test.go

@ -4,15 +4,15 @@ import (
"net"
"testing"
"git.aiterp.net/gisle/irc/internal/irctest"
"github.com/gissleh/irc/internal/irctest"
)
func TestInteraction(t *testing.T) {
interaction := irctest.Interaction{
Lines: []irctest.InteractionLine{
{Kind: 'C', Data: "FIRST MESSAGE"},
{Kind: 'S', Data: "SERVER MESSAGE"},
{Kind: 'C', Data: "SECOND MESSAGE"},
{Client: "FIRST MESSAGE"},
{Server: "SERVER MESSAGE"},
{Client: "SECOND MESSAGE"},
},
}

2
ircutil/cut-message_test.go

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"git.aiterp.net/gisle/irc/ircutil"
"github.com/gissleh/irc/ircutil"
)
func TestCuts(t *testing.T) {

78
isupport/isupport.go

@ -12,20 +12,15 @@ import (
// of it. It's thread-safe through a reader/writer lock, so the locks will
// only block in the short duration post-registration when the 005s come in
type ISupport struct {
lock sync.RWMutex
raw map[string]string
prefixes map[rune]rune
modeOrder string
prefixOrder string
chanModes []string
lock sync.RWMutex
state State
}
// Get gets an isupport key. This is unprocessed data, and a helper should
// be used if available.
func (isupport *ISupport) Get(key string) (value string, ok bool) {
isupport.lock.RLock()
value, ok = isupport.raw[key]
value, ok = isupport.state.Raw[key]
isupport.lock.RUnlock()
return
}
@ -33,7 +28,7 @@ func (isupport *ISupport) Get(key string) (value string, ok bool) {
// Number gets a key and converts it to a number.
func (isupport *ISupport) Number(key string) (value int, ok bool) {
isupport.lock.RLock()
strValue, ok := isupport.raw[key]
strValue, ok := isupport.state.Raw[key]
isupport.lock.RUnlock()
if !ok {
@ -54,12 +49,12 @@ func (isupport *ISupport) ParsePrefixedNick(fullnick string) (nick, modes, prefi
isupport.lock.RLock()
defer isupport.lock.RUnlock()
if fullnick == "" || isupport.prefixes == nil {
if fullnick == "" || isupport.state.Prefixes == nil {
return fullnick, "", ""
}
for i, ch := range fullnick {
if mode, ok := isupport.prefixes[ch]; ok {
if mode, ok := isupport.state.Prefixes[ch]; ok {
modes += string(mode)
prefixes += string(ch)
} else {
@ -80,7 +75,7 @@ func (isupport *ISupport) HighestPrefix(prefixes string) rune {
return rune(prefixes[0])
}
for _, prefix := range isupport.prefixOrder {
for _, prefix := range isupport.state.PrefixOrder {
if strings.ContainsRune(prefixes, prefix) {
return prefix
}
@ -98,7 +93,7 @@ func (isupport *ISupport) HighestMode(modes string) rune {
return rune(modes[0])
}
for _, mode := range isupport.modeOrder {
for _, mode := range isupport.state.ModeOrder {
if strings.ContainsRune(modes, mode) {
return mode
}
@ -122,7 +117,7 @@ func (isupport *ISupport) IsModeHigher(current rune, other rune) bool {
return true
}
for _, mode := range isupport.modeOrder {
for _, mode := range isupport.state.ModeOrder {
if mode == current {
return true
} else if mode == other {
@ -137,7 +132,7 @@ func (isupport *ISupport) IsModeHigher(current rune, other rune) bool {
func (isupport *ISupport) SortModes(modes string) string {
result := ""
for _, ch := range isupport.modeOrder {
for _, ch := range isupport.state.ModeOrder {
for _, ch2 := range modes {
if ch2 == ch {
result += string(ch)
@ -152,7 +147,7 @@ func (isupport *ISupport) SortModes(modes string) string {
func (isupport *ISupport) SortPrefixes(prefixes string) string {
result := ""
for _, ch := range isupport.prefixOrder {
for _, ch := range isupport.state.PrefixOrder {
for _, ch2 := range prefixes {
if ch2 == ch {
result += string(ch)
@ -168,7 +163,7 @@ func (isupport *ISupport) Mode(prefix rune) rune {
isupport.lock.RLock()
defer isupport.lock.RUnlock()
return isupport.prefixes[prefix]
return isupport.state.Prefixes[prefix]
}
// Prefix gets the prefix for the mode. It's a bit slower
@ -178,7 +173,7 @@ func (isupport *ISupport) Prefix(mode rune) rune {
isupport.lock.RLock()
defer isupport.lock.RUnlock()
for prefix, mappedMode := range isupport.prefixes {
for prefix, mappedMode := range isupport.state.Prefixes {
if mappedMode == mode {
return prefix
}
@ -207,7 +202,7 @@ func (isupport *ISupport) IsChannel(targetName string) bool {
isupport.lock.RLock()
defer isupport.lock.RUnlock()
return strings.Contains(isupport.raw["CHANTYPES"], string(targetName[0]))
return strings.Contains(isupport.state.Raw["CHANTYPES"], string(targetName[0]))
}
// IsPermissionMode returns whether the flag is a permission mode
@ -215,7 +210,7 @@ func (isupport *ISupport) IsPermissionMode(flag rune) bool {
isupport.lock.RLock()
defer isupport.lock.RUnlock()
return strings.ContainsRune(isupport.modeOrder, flag)
return strings.ContainsRune(isupport.state.ModeOrder, flag)
}
// ModeTakesArgument returns true if the mode takes an argument
@ -224,17 +219,17 @@ func (isupport *ISupport) ModeTakesArgument(flag rune, plus bool) bool {
defer isupport.lock.RUnlock()
// Permission modes always take an argument.
if strings.ContainsRune(isupport.modeOrder, flag) {
if strings.ContainsRune(isupport.state.ModeOrder, flag) {
return true
}
// Modes in category A and B always takes an argument
if strings.ContainsRune(isupport.chanModes[0], flag) || strings.ContainsRune(isupport.chanModes[1], flag) {
if strings.ContainsRune(isupport.state.ChannelModes[0], flag) || strings.ContainsRune(isupport.state.ChannelModes[1], flag) {
return true
}
// Modes in category C only takes one when added
if plus && strings.ContainsRune(isupport.chanModes[1], flag) {
if plus && strings.ContainsRune(isupport.state.ChannelModes[1], flag) {
return true
}
@ -251,11 +246,11 @@ func (isupport *ISupport) ChannelModeType(mode rune) int {
// User permission modes function exactly like the first block
// when it comes to add/remove
if strings.ContainsRune(isupport.modeOrder, mode) {
if strings.ContainsRune(isupport.state.ModeOrder, mode) {
return 0
}
for i, block := range isupport.chanModes {
for i, block := range isupport.state.ChannelModes {
if strings.ContainsRune(block, mode) {
return i
}
@ -272,43 +267,48 @@ func (isupport *ISupport) Set(key, value string) {
isupport.lock.Lock()
if isupport.raw == nil {
isupport.raw = make(map[string]string, 32)
if isupport.state.Raw == nil {
isupport.state.Raw = make(map[string]string, 32)
}
isupport.raw[key] = value
isupport.state.Raw[key] = value
switch key {
case "PREFIX": // PREFIX=(ov)@+
{
split := strings.SplitN(value[1:], ")", 2)
isupport.prefixOrder = split[1]
isupport.modeOrder = split[0]
isupport.prefixes = make(map[rune]rune, len(split[0]))
isupport.state.PrefixOrder = split[1]
isupport.state.ModeOrder = split[0]
isupport.state.Prefixes = make(map[rune]rune, len(split[0]))
for i, ch := range split[0] {
isupport.prefixes[rune(split[1][i])] = ch
isupport.state.Prefixes[rune(split[1][i])] = ch
}
}
case "CHANMODES": // CHANMODES=eIbq,k,flj,CFLNPQcgimnprstz
{
isupport.chanModes = strings.Split(value, ",")
isupport.state.ChannelModes = strings.Split(value, ",")
}
}
isupport.lock.Unlock()
}
// State gets a copy of the isupport state.
func (isupport *ISupport) State() *State {
return isupport.state.Copy()
}
// Reset clears everything.
func (isupport *ISupport) Reset() {
isupport.lock.Lock()
isupport.prefixOrder = ""
isupport.modeOrder = ""
isupport.prefixes = nil
isupport.chanModes = nil
isupport.state.PrefixOrder = ""
isupport.state.ModeOrder = ""
isupport.state.Prefixes = nil
isupport.state.ChannelModes = nil
for key := range isupport.raw {
delete(isupport.raw, key)
for key := range isupport.state.Raw {
delete(isupport.state.Raw, key)
}
isupport.lock.Unlock()
}

83
isupport/isupport_test.go

@ -0,0 +1,83 @@
package isupport_test
import (
"github.com/gissleh/irc/isupport"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
var isupportMessages = "FNC SAFELIST ELIST=CTU MONITOR=100 WHOX ETRACE KNOCK CHANTYPES=#& EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLNPQcgimnprstz CHANLIMIT=#&:15 PREFIX=(aovh)~@+% MAXLIST=bqeI:100 MODES=4 NETWORK=TestServer STATUSMSG=@+% CALLERID=g CASEMAPPING=rfc1459 NICKLEN=30 MAXNICKLEN=31 CHANNELLEN=50 TOPICLEN=390 DEAF=D TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,&acjmorsuxz| CLIENTVER=3.0"
var is isupport.ISupport
func init() {
for _, token := range strings.Split(isupportMessages, " ") {
pair := strings.SplitN(token, "=", 2)
if len(pair) == 2 {
is.Set(pair[0], pair[1])
} else {
is.Set(pair[0], "")
}
}
}
func TestISupport_ParsePrefixedNick(t *testing.T) {
table := []struct {
Full string
Prefixes string
Modes string
Nick string
}{
{"User", "", "", "User"},
{"+User", "+", "v", "User"},
{"@%+User", "@%+", "ohv", "User"},
{"~User", "~", "a", "User"},
}
for _, row := range table {
t.Run(row.Full, func(t *testing.T) {
nick, modes, prefixes := is.ParsePrefixedNick(row.Full)
assert.Equal(t, row.Nick, nick)
assert.Equal(t, row.Modes, modes)
assert.Equal(t, row.Prefixes, prefixes)
})
}
}
func TestISupport_IsChannel(t *testing.T) {
table := map[string]bool{
"#Test": true,
"&Test": true,
"User": false,
"+Stuff": false,
"#TestAndSuch": true,
"@astrwef": false,
}
for channelName, isChannel := range table {
t.Run(channelName, func(t *testing.T) {
assert.Equal(t, isChannel, is.IsChannel(channelName))
})
}
}
func TestISupport_IsPermissionMode(t *testing.T) {
table := map[rune]bool{
'#': false,
'+': false,
'o': true,
'v': true,
'h': true,
'a': true,
'g': false,
'p': false,
}
for flag, expected := range table {
t.Run(string(flag), func(t *testing.T) {
assert.Equal(t, expected, is.IsPermissionMode(flag))
})
}
}

19
isupport/state.go

@ -0,0 +1,19 @@
package isupport
type State struct {
Raw map[string]string `json:"raw"`
Prefixes map[rune]rune `json:"-"`
ModeOrder string `json:"modeOrder"`
PrefixOrder string `json:"prefixOrder"`
ChannelModes []string `json:"channelModes"`