diff --git a/.drone.yml b/.drone.yml
index 6354f94..ae7b0f8 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -1,6 +1,6 @@
workspace:
base: /go
- path: src/git.aiterp.net/gisle/irc
+ path: src/github.com/gissleh/irc
pipeline:
test:
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..e7e9d11
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,2 @@
+# Default ignored files
+/workspace.xml
diff --git a/.idea/dictionaries/gisle.xml b/.idea/dictionaries/gisle.xml
new file mode 100644
index 0000000..30f8fb8
--- /dev/null
+++ b/.idea/dictionaries/gisle.xml
@@ -0,0 +1,18 @@
+
+
+
+ chanmodes
+ chantypes
+ ctcp
+ fullnick
+ invalidcommand
+ isupport
+ polyfilled
+ privmsg
+ qcgimnprstz
+ structs
+ testserver
+ tstate
+
+
+
\ No newline at end of file
diff --git a/.idea/irc.iml b/.idea/irc.iml
new file mode 100644
index 0000000..c956989
--- /dev/null
+++ b/.idea/irc.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..28a804d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..6560466
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml
new file mode 100644
index 0000000..97ad6d2
--- /dev/null
+++ b/.idea/watcherTasks.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/channel.go b/channel.go
index bba988d..bb70853 100644
--- a/channel.go
+++ b/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":
diff --git a/client.go b/client.go
index c668bd4..fd3429a 100644
--- a/client.go
+++ b/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()
}
diff --git a/client_test.go b/client_test.go
index f58dff2..8b90eba 100644
--- a/client_test.go
+++ b/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
diff --git a/cmd/ircrepl/main.go b/cmd/ircrepl/main.go
index 4b0cedb..6f442f7 100644
--- a/cmd/ircrepl/main.go
+++ b/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)
}
}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..75e70a0
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/gissleh/irc
+
+go 1.12
+
+require github.com/stretchr/testify v1.4.0
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..6379896
--- /dev/null
+++ b/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=
diff --git a/handle.go b/handle.go
index 821d497..9236dbd 100644
--- a/handle.go
+++ b/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)
}
diff --git a/handle_test.go b/handle_test.go
index 7a4a265..a3c8af9 100644
--- a/handle_test.go
+++ b/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 {
diff --git a/handlers/input.go b/handlers/input.go
index a55212d..c6510f7 100644
--- a/handlers/input.go
+++ b/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)
+ }
}
}
diff --git a/handlers/mroleplay.go b/handlers/mroleplay.go
index caaa4af..ac9ab23 100644
--- a/handlers/mroleplay.go
+++ b/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
diff --git a/internal/irctest/assert.go b/internal/irctest/assert.go
index acbbd4c..12143c8 100644
--- a/internal/irctest/assert.go
+++ b/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
diff --git a/internal/irctest/interaction.go b/internal/irctest/interaction.go
index d3fee94..667d1e2 100644
--- a/internal/irctest/interaction.go
+++ b/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
}
diff --git a/internal/irctest/interaction_test.go b/internal/irctest/interaction_test.go
index 05b52c4..cf0c2c2 100644
--- a/internal/irctest/interaction_test.go
+++ b/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"},
},
}
diff --git a/ircutil/cut-message_test.go b/ircutil/cut-message_test.go
index 9bdb9fd..3b8585b 100644
--- a/ircutil/cut-message_test.go
+++ b/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) {
diff --git a/isupport/isupport.go b/isupport/isupport.go
index eb18634..ac55df1 100644
--- a/isupport/isupport.go
+++ b/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()
}
diff --git a/isupport/isupport_test.go b/isupport/isupport_test.go
new file mode 100644
index 0000000..88ff37d
--- /dev/null
+++ b/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))
+ })
+ }
+}
diff --git a/isupport/state.go b/isupport/state.go
new file mode 100644
index 0000000..79d77c6
--- /dev/null
+++ b/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"`
+}
+
+func (state *State) Copy() *State {
+ stateCopy := *state
+ stateCopy.Raw = make(map[string]string, len(state.Raw))
+ for key, value := range state.Raw {
+ stateCopy.Raw[key] = value
+ }
+
+ return &stateCopy
+}
diff --git a/list/list.go b/list/list.go
index 27a9da4..2b70763 100644
--- a/list/list.go
+++ b/list/list.go
@@ -5,7 +5,7 @@ import (
"strings"
"sync"
- "git.aiterp.net/gisle/irc/isupport"
+ "github.com/gissleh/irc/isupport"
)
// The List of users in a channel. It has all operations one would perform on
diff --git a/list/list_test.go b/list/list_test.go
index ddc2391..52b72a1 100644
--- a/list/list_test.go
+++ b/list/list_test.go
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
- "git.aiterp.net/gisle/irc/isupport"
- "git.aiterp.net/gisle/irc/list"
+ "github.com/gissleh/irc/isupport"
+ "github.com/gissleh/irc/list"
)
var testISupport isupport.ISupport
diff --git a/query.go b/query.go
index 86144e1..fb9361b 100644
--- a/query.go
+++ b/query.go
@@ -1,7 +1,7 @@
package irc
import (
- "git.aiterp.net/gisle/irc/list"
+ "github.com/gissleh/irc/list"
)
// A Query is a target for direct messages to and from a specific nick.
@@ -19,7 +19,15 @@ func (query *Query) Name() string {
return query.user.Nick
}
-// Handle handles messages routed to this channel by the client's event loop
+func (query *Query) State() TargetState {
+ return TargetState{
+ Kind: "query",
+ Name: query.user.Nick,
+ Users: []list.User{query.user},
+ }
+}
+
+// AddHandler handles messages routed to this channel by the client's event loop
func (query *Query) Handle(event *Event, client *Client) {
switch event.Name() {
case "packet.nick":
diff --git a/state.go b/state.go
new file mode 100644
index 0000000..37e8761
--- /dev/null
+++ b/state.go
@@ -0,0 +1,24 @@
+package irc
+
+import (
+ "github.com/gissleh/irc/isupport"
+ "github.com/gissleh/irc/list"
+)
+
+type ClientState struct {
+ Nick string `json:"nick"`
+ User string `json:"user"`
+ Host string `json:"host"`
+ Connected bool `json:"connected"`
+ Ready bool `json:"quit"`
+ ISupport *isupport.State `json:"isupport"`
+ Caps []string `json:"caps"`
+ Targets []TargetState `json:"targets"`
+}
+
+type TargetState struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Name string `json:"name"`
+ Users []list.User `json:"users,omitempty"`
+}
diff --git a/status.go b/status.go
index 71f818d..30c6f34 100644
--- a/status.go
+++ b/status.go
@@ -14,7 +14,15 @@ func (status *Status) Name() string {
return "Status"
}
-// Handle handles messages routed to this status by the client's event loop
+func (status *Status) State() TargetState {
+ return TargetState{
+ Kind: "status",
+ Name: "Status",
+ Users: nil,
+ }
+}
+
+// AddHandler handles messages routed to this status by the client's event loop
func (status *Status) Handle(event *Event, client *Client) {
}
diff --git a/target.go b/target.go
index c76dc75..0405c44 100644
--- a/target.go
+++ b/target.go
@@ -6,4 +6,5 @@ type Target interface {
Kind() string
Name() string
Handle(event *Event, client *Client)
+ State() TargetState
}