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 }