├── api └── proto.sh ├── AUTHORS ├── data ├── provider.go ├── channeluser_meta.go ├── json_storer_test.go ├── channeluser_meta_test.go ├── user.go ├── json_storer.go ├── user_modes.go ├── stored_channel_test.go ├── stored_channel.go ├── user_test.go ├── user_modes_test.go ├── channel.go ├── channel_modes_diff.go ├── access.go ├── channel_modes_diff_test.go ├── access_test.go ├── channel_test.go └── modes_common_test.go ├── registrar ├── registrar.go ├── proxy_test.go ├── proxy.go ├── holder.go └── holder_test.go ├── .gitignore ├── dispatch ├── dispatch_core_test.go ├── cmd │ ├── scopes.go │ ├── event.go │ ├── command_args_test.go │ └── command.go ├── dispatch_core.go ├── dispatcher_test.go ├── dispatcher.go ├── trie.go └── remote │ └── client.go ├── circle.yml ├── inet ├── queue.go └── queue_test.go ├── bot ├── extlocal.go ├── run.go ├── extlocal_test.go ├── botconfig.go ├── core_handler.go ├── pipe.go ├── botconfig_test.go ├── server_test.go └── core_handler_test.go ├── LICENSE ├── parse ├── parse.go └── parse_test.go ├── mocks └── conn_mock.go ├── irc ├── event_test.go ├── event.go ├── ctcp.go ├── hosts.go ├── network_info_test.go ├── proto.go ├── ctcp_test.go └── hosts_test.go ├── config ├── map_helpers_test.go ├── config_file.go ├── map_helpers.go ├── config_test.go └── network_test.go ├── plan.txt └── README.md /api/proto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | protoc -I ./ *.proto --go_out=plugins=grpc:./ 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The authors of the Ultimateq IRC bot/framework. 2 | 3 | Aaron L 4 | Anton Lindgren 5 | -------------------------------------------------------------------------------- /data/provider.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // Provider can provide a state or store database upon request. Either can be 4 | // nil even if requested. 5 | type Provider interface { 6 | State(network string) *State 7 | Store() *Store 8 | } 9 | -------------------------------------------------------------------------------- /registrar/registrar.go: -------------------------------------------------------------------------------- 1 | package registrar 2 | 3 | import ( 4 | "github.com/aarondl/ultimateq/dispatch" 5 | "github.com/aarondl/ultimateq/dispatch/cmd" 6 | ) 7 | 8 | // Interface is the operations performable by a registrar. 9 | type Interface interface { 10 | Register(network, channel, event string, handler dispatch.Handler) uint64 11 | RegisterCmd(network, channel string, command *cmd.Command) (uint64, error) 12 | 13 | Unregister(id uint64) bool 14 | UnregisterCmd(id uint64) bool 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | # Vim 25 | tags 26 | 27 | # Extras 28 | ultimateq 29 | *.yaml 30 | *.toml 31 | *.db 32 | *.sqlite3 33 | todo* 34 | .1fe85649d7f7b2110f5e83078076942ad065445f 35 | -------------------------------------------------------------------------------- /dispatch/dispatch_core_test.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aarondl/ultimateq/irc" 7 | ) 8 | 9 | var netInfo = irc.NewNetworkInfo() 10 | 11 | func TestCore(t *testing.T) { 12 | t.Parallel() 13 | d := NewCore(nil) 14 | if d == nil { 15 | t.Error("Create should create things.") 16 | } 17 | } 18 | 19 | func TestCore_Synchronization(t *testing.T) { 20 | t.Parallel() 21 | d := NewCore(nil) 22 | d.HandlerStarted() 23 | d.HandlerStarted() 24 | d.HandlerFinished() 25 | d.HandlerFinished() 26 | d.WaitForHandlers() 27 | } 28 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | pre: 3 | - go get github.com/jstemmer/go-junit-report 4 | - go get github.com/mattn/goveralls 5 | override: 6 | - go test -v -race ./... > $CIRCLE_ARTIFACTS/gotest.txt 7 | - > 8 | echo "mode: set" > $CIRCLE_ARTIFACTS/coverage.txt && 9 | for i in $(go list ./...); do 10 | rm -f coverage.tmp; 11 | go test -v -coverprofile coverage.tmp $i; 12 | tail -n +2 coverage.tmp >> $CIRCLE_ARTIFACTS/coverage.txt; 13 | done 14 | post: 15 | - cat $CIRCLE_ARTIFACTS/gotest.txt | go-junit-report > $CIRCLE_TEST_REPORTS/junit.xml 16 | - goveralls -coverprofile=$CIRCLE_ARTIFACTS/coverage.txt -service=circle-ci -repotoken=$COVERALLS_TOKEN 17 | -------------------------------------------------------------------------------- /data/channeluser_meta.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // channelUser represents a user that's on a channel. 4 | type channelUser struct { 5 | User *User 6 | *UserModes 7 | } 8 | 9 | // newChannelUser creates a channel user that represents a channel that 10 | // contains a user. 11 | func newChannelUser(u *User, m *UserModes) channelUser { 12 | return channelUser{ 13 | User: u, 14 | UserModes: m, 15 | } 16 | } 17 | 18 | // userChannel represents a user that's on a channel. 19 | type userChannel struct { 20 | Channel *Channel 21 | *UserModes 22 | } 23 | 24 | // newUserChannel creates a user channel that represents a user that is 25 | // on a channel. 26 | func newUserChannel(c *Channel, m *UserModes) userChannel { 27 | return userChannel{ 28 | Channel: c, 29 | UserModes: m, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /data/json_storer_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "testing" 4 | 5 | func TestJSONStorer(t *testing.T) { 6 | t.Parallel() 7 | 8 | js := make(JSONStorer) 9 | js.Put("Hello", "world") 10 | if got, ok := js.Get("Hello"); !ok || got != "world" { 11 | t.Error("Expected world, got", got) 12 | } 13 | 14 | st := []struct { 15 | Name string 16 | Age int 17 | }{ 18 | {"zamn", 21}, 19 | {}, 20 | } 21 | 22 | err := js.PutJSON(st[0].Name, st[0]) 23 | if err != nil { 24 | t.Error("Unexpected marshalling error:", err) 25 | } 26 | 27 | if ok, err := js.GetJSON(st[0].Name, &st[1]); !ok { 28 | t.Error("Key not found:", ok) 29 | } else if err != nil { 30 | t.Error("Error unmarshalling value:", err) 31 | } else if st[0] != st[1] { 32 | t.Errorf("Structs do not match %#v %#v", st[0], st[1]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dispatch/cmd/scopes.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Kind is the kind of messages to listen to. 4 | type Kind int 5 | 6 | // Scope is the scope of the messages to listen to. 7 | type Scope int 8 | 9 | // Constants used for defining the targets/scope of a command. 10 | const ( 11 | // KindPrivmsg only listens to irc.PRIVMSG events. 12 | Privmsg Kind = 0x1 13 | // KindNotice only listens to irc.NOTICE events. 14 | Notice Kind = 0x2 15 | // AnyKind listens to both irc.PRIVMSG and irc.NOTICE events. 16 | AnyKind Kind = 0x3 17 | 18 | // Private only listens to PRIVMSG or NOTICE sent directly to the bot. 19 | Private Scope = 0x1 20 | // PUBLIC only listens to PRIVMSG or NOTICE sent to a channel. 21 | Public Scope = 0x2 22 | // AnyScope listens to events sent to a channel or directly to the bot. 23 | AnyScope Scope = 0x3 24 | ) 25 | -------------------------------------------------------------------------------- /data/channeluser_meta_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var modes = new(int) 8 | 9 | func TestChannelUser(t *testing.T) { 10 | t.Parallel() 11 | 12 | user := NewUser("nick") 13 | modes := NewUserModes(testKinds) 14 | 15 | cu := newChannelUser(user, &modes) 16 | 17 | if got, exp := cu.User, user; exp != got { 18 | t.Errorf("Expected: %v, got: %v", exp, got) 19 | } 20 | if got, exp := cu.UserModes, &modes; exp != got { 21 | t.Errorf("Expected: %v, got: %v", exp, got) 22 | } 23 | } 24 | 25 | func TestUserChannel(t *testing.T) { 26 | t.Parallel() 27 | 28 | ch := NewChannel("", testKinds) 29 | modes := NewUserModes(testKinds) 30 | 31 | uc := newUserChannel(ch, &modes) 32 | 33 | if got, exp := uc.Channel, ch; exp != got { 34 | t.Errorf("Expected: %v, got: %v", exp, got) 35 | } 36 | if got, exp := uc.UserModes, &modes; exp != got { 37 | t.Errorf("Expected: %v, got: %v", exp, got) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /inet/queue.go: -------------------------------------------------------------------------------- 1 | package inet 2 | 3 | // queueNode is the node structure underneath the Queue type. 4 | type queueNode struct { 5 | next *queueNode 6 | data *[]byte 7 | } 8 | 9 | // Queue implements a singly-linked queue data structure for byte slices. 10 | type Queue struct { 11 | front *queueNode 12 | back *queueNode 13 | length int 14 | } 15 | 16 | // Enqueue adds the byte slice to the queue 17 | func (q *Queue) Enqueue(bytes []byte) { 18 | if len(bytes) == 0 { 19 | return 20 | } 21 | 22 | node := &queueNode{data: &bytes} 23 | 24 | if q.length == 0 { 25 | q.front = node 26 | q.back = q.front 27 | } else { 28 | q.back.next = node 29 | q.back = node 30 | } 31 | 32 | q.length++ 33 | } 34 | 35 | // Dequeue dequeues from the front of the queue. 36 | func (q *Queue) Dequeue() []byte { 37 | if q.length == 0 { 38 | return nil 39 | } 40 | data := q.front.data 41 | q.front = q.front.next 42 | if q.length == 1 { 43 | q.back = nil 44 | } 45 | q.length-- 46 | 47 | return *data 48 | } 49 | -------------------------------------------------------------------------------- /data/user.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/aarondl/ultimateq/api" 5 | "github.com/aarondl/ultimateq/irc" 6 | ) 7 | 8 | // User encapsulates all the data associated with a user. 9 | type User struct { 10 | irc.Host `json:"host"` 11 | Realname string `json:"realname"` 12 | } 13 | 14 | // NewUser creates a user object from a nickname or fullhost. 15 | func NewUser(nickorhost string) *User { 16 | if len(nickorhost) == 0 { 17 | return nil 18 | } 19 | 20 | return &User{ 21 | Host: irc.Host(nickorhost), 22 | } 23 | } 24 | 25 | // String returns a one-line representation of this user. 26 | func (u *User) String() string { 27 | str := u.Host.Nick() 28 | if fh := u.Host.String(); len(fh) > 0 && str != fh { 29 | str += " " + fh 30 | } 31 | if len(u.Realname) > 0 { 32 | str += " " + u.Realname 33 | } 34 | 35 | return str 36 | } 37 | 38 | // ToProto converts stateuser to a protocol buffer 39 | func (u *User) ToProto() *api.StateUser { 40 | user := new(api.StateUser) 41 | user.Host = string(u.Host) 42 | user.Realname = u.Realname 43 | 44 | return user 45 | } 46 | -------------------------------------------------------------------------------- /bot/extlocal.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | // Extension provides methods to initialize and de-initialize itself. 4 | // The bot type is passed in as the interface with which to register event 5 | // handlers, commands, and access the databases. The de-init should destroy the 6 | // extension until it is called upon again. 7 | // 8 | // Event handlers can execute in parallel and so there must be provisions to 9 | // protect mutable state within the implementation. 10 | type Extension interface { 11 | // Init attaches event handlers and commands 12 | Init(*Bot) error 13 | // Deinit detaches event handlers and commands 14 | Deinit(*Bot) error 15 | } 16 | 17 | var extensions = map[string]Extension{} 18 | 19 | // RegisterExtension with the bot. This should be called on init(), and 20 | // the extension should rely upon import side effects in order to have that 21 | // init() called. Panics if a name is registered twice. 22 | func RegisterExtension(name string, ext Extension) { 23 | if _, ok := extensions[name]; ok { 24 | panic("Extension [" + name + "] is already registered.") 25 | } 26 | 27 | extensions[name] = ext 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Aaron Lefkowitz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /registrar/proxy_test.go: -------------------------------------------------------------------------------- 1 | package registrar 2 | 3 | import "testing" 4 | 5 | func TestProxy_New(t *testing.T) { 6 | t.Parallel() 7 | 8 | m := &mockReg{} 9 | p := NewProxy(m) 10 | 11 | if p.holders == nil { 12 | t.Error("holders not initialized") 13 | } 14 | } 15 | 16 | func TestProxy_Get(t *testing.T) { 17 | t.Parallel() 18 | 19 | m := &mockReg{} 20 | p := NewProxy(m) 21 | 22 | if ln := len(p.holders); ln != 0 { 23 | t.Error("should be empty:", ln) 24 | } 25 | 26 | i := p.Get("test") 27 | if _, ok := i.(*holder); !ok { 28 | t.Errorf("wrong type: %T", i) 29 | } 30 | 31 | if ln := len(p.holders); ln != 1 { 32 | t.Error("should have one:", ln) 33 | } 34 | 35 | again := p.Get("test") 36 | if i != again { 37 | t.Error("should re-use existing holders") 38 | } 39 | 40 | if ln := len(p.holders); ln != 1 { 41 | t.Error("should have one:", ln) 42 | } 43 | } 44 | 45 | func TestProxy_Unregister(t *testing.T) { 46 | t.Parallel() 47 | 48 | m := &mockReg{} 49 | p := NewProxy(m) 50 | 51 | p.Get("hello").Register("n", "c", "e", nil) 52 | p.Get("hello").Register("n", "c", "e", nil) 53 | 54 | p.Unregister("hello") 55 | 56 | m.verifyMock(t, 2, 2, 0, 0) 57 | 58 | if len(p.holders) != 0 { 59 | t.Error("should be empty") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /data/json_storer.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "encoding/json" 4 | 5 | // JSONStorer allows storage of normal strings and json values into a map. 6 | type JSONStorer map[string]string 7 | 8 | // Clone does a deep copy of the JSONStorer. 9 | func (js JSONStorer) Clone() JSONStorer { 10 | j := make(JSONStorer, len(js)) 11 | for k, v := range js { 12 | j[k] = v 13 | } 14 | return j 15 | } 16 | 17 | // Put puts a regular string into the map. 18 | func (js JSONStorer) Put(key, value string) { 19 | js[key] = value 20 | } 21 | 22 | // Get gets a regular string from the map. 23 | func (js JSONStorer) Get(key string) (string, bool) { 24 | ret, ok := js[key] 25 | return ret, ok 26 | } 27 | 28 | // PutJSON serializes the value and stores it in the map. 29 | func (js JSONStorer) PutJSON(key string, value interface{}) error { 30 | jsMarsh, err := json.Marshal(value) 31 | if err != nil { 32 | return err 33 | } 34 | js[key] = string(jsMarsh) 35 | return nil 36 | } 37 | 38 | // GetJSON deserializes the value from the map and stores it in intf. 39 | func (js JSONStorer) GetJSON(key string, intf interface{}) (bool, error) { 40 | ret, ok := js[key] 41 | if !ok { 42 | return false, nil 43 | } 44 | 45 | err := json.Unmarshal([]byte(ret), intf) 46 | return true, err 47 | } 48 | -------------------------------------------------------------------------------- /registrar/proxy.go: -------------------------------------------------------------------------------- 1 | package registrar 2 | 3 | // Proxy all the registrations through a storage mechanism, the important 4 | // thing about proxy is that it adds a "name" layer to each registration 5 | // for say extensions or the like. It also has a provision for unregistering 6 | // any of the things by its proxied by name. 7 | // 8 | // Proxy is not safe to use from multiple goroutines without additional 9 | // synchronization 10 | type Proxy struct { 11 | registrar Interface 12 | holders map[string]*holder 13 | } 14 | 15 | // NewProxy constructor, holds a reference to the interface passed in. 16 | func NewProxy(registrar Interface) *Proxy { 17 | p := &Proxy{ 18 | registrar: registrar, 19 | holders: make(map[string]*holder), 20 | } 21 | 22 | return p 23 | } 24 | 25 | // Get a proxying object and a kill channel for it. Creates a new one if 26 | // one is not found. 27 | func (p *Proxy) Get(name string) Interface { 28 | holder, ok := p.holders[name] 29 | if ok { 30 | return holder 31 | } 32 | 33 | holder = newHolder(p.registrar) 34 | p.holders[name] = holder 35 | return holder 36 | } 37 | 38 | // Unregister everything registered to name 39 | func (p *Proxy) Unregister(name string) { 40 | holder, ok := p.holders[name] 41 | if !ok { 42 | return 43 | } 44 | delete(p.holders, name) 45 | 46 | holder.unregisterAll() 47 | } 48 | -------------------------------------------------------------------------------- /dispatch/dispatch_core.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dispatch is used to dispatch irc messages to event handlers in an 3 | concurrent fashion. It supports various event handler types to easily 4 | extract information from events, as well as define more succint handlers. 5 | */ 6 | package dispatch 7 | 8 | import ( 9 | "runtime" 10 | "sync" 11 | 12 | "gopkg.in/inconshreveable/log15.v2" 13 | ) 14 | 15 | //Core is a core for any dispatching mechanisms that includes a sync'd 16 | // a waiter to synchronize the exit of all the event handlers sharing this core. 17 | type Core struct { 18 | log log15.Logger 19 | waiter sync.WaitGroup 20 | protect sync.RWMutex 21 | } 22 | 23 | // NewCore initializes a dispatch core 24 | func NewCore(logger log15.Logger) *Core { 25 | d := &Core{log: logger} 26 | 27 | return d 28 | } 29 | 30 | // HandlerStarted tells the core that a handler has started and it should be 31 | // waited on. 32 | func (d *Core) HandlerStarted() { 33 | d.waiter.Add(1) 34 | } 35 | 36 | // HandlerFinished tells the core that a handler has ended. 37 | func (d *Core) HandlerFinished() { 38 | d.waiter.Done() 39 | } 40 | 41 | // WaitForHandlers waits for the unfinished handlers to finish. 42 | func (d *Core) WaitForHandlers() { 43 | d.waiter.Wait() 44 | } 45 | 46 | // PanicHandler catches any panics and logs a stack trace 47 | func (d *Core) PanicHandler() { 48 | recovered := recover() 49 | if recovered == nil { 50 | return 51 | } 52 | buf := make([]byte, 1024) 53 | runtime.Stack(buf, false) 54 | d.log.Error("Handler failed", "panic", recovered) 55 | d.log.Error(string(buf)) 56 | } 57 | -------------------------------------------------------------------------------- /bot/run.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "time" 7 | 8 | "github.com/aarondl/ultimateq/config" 9 | ) 10 | 11 | // Run makes a very typical bot. It will call the cb function passed in 12 | // before starting to allow registration of extensions etc. Returns error 13 | // if the bot could not be created. Does NOT return until dead. 14 | // The following are featured behaviors: 15 | // Reads configuration file from ./config.toml 16 | // Watches for Keyboard Input OR SIGTERM OR SIGKILL and shuts down normally. 17 | // Pauses after death to allow all goroutines to come to a graceful shutdown. 18 | func Run(cb func(b *Bot)) error { 19 | cfg := config.New().FromFile("config.toml") 20 | b, err := New(cfg) 21 | if err != nil { 22 | return err 23 | } 24 | defer b.Close() 25 | 26 | cb(b) 27 | 28 | end := b.Start() 29 | 30 | _, ok := cfg.ExtGlobal().Listen() 31 | if ok { 32 | api := NewAPIServer(b) 33 | go func() { 34 | err := api.Start() 35 | if err != nil { 36 | b.Logger.Error("failed to start apiserver", "err", err) 37 | } 38 | }() 39 | } 40 | 41 | input, quit := make(chan int), make(chan os.Signal, 2) 42 | 43 | /*go func() { 44 | scanner := bufio.NewScanner(os.Stdin) 45 | scanner.Scan() 46 | input <- 0 47 | }()*/ 48 | 49 | signal.Notify(quit, os.Interrupt, os.Kill) 50 | 51 | stop := false 52 | for !stop { 53 | select { 54 | case <-input: 55 | b.Stop() 56 | stop = true 57 | case <-quit: 58 | b.Stop() 59 | stop = true 60 | case err, ok := <-end: 61 | if ok { 62 | b.Info("Server death", "err", err) 63 | } 64 | stop = !ok 65 | } 66 | } 67 | 68 | b.Cleanup() 69 | b.Info("Shutting down...") 70 | <-time.After(1 * time.Second) 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /parse/parse.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package parse has functions to parse the irc protocol into irc.IrcMessages. 3 | */ 4 | package parse 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/aarondl/ultimateq/irc" 11 | ) 12 | 13 | const ( 14 | // errMsgParseFailure is given when the ircRegex fails to parse the protocol 15 | errMsgParseFailure = "parse: Unable to parse received irc protocol" 16 | ) 17 | 18 | var ( 19 | // ircRegex is used to parse the parts of irc protocol. 20 | ircRegex = regexp.MustCompile( 21 | `^(?::(\S+) )?([A-Z0-9]+)((?: (?:[^:\s][^\s]*))*)(?: :(.*))?\s*$`) 22 | ) 23 | 24 | // ParseError is generated when something does not match the regex, irc.Parse 25 | // will return one of these containing the invalid seeming irc protocol string. 26 | type ParseError struct { 27 | // The invalid irc encountered. 28 | Irc string 29 | } 30 | 31 | // Error satisfies the Error interface for ParseError. 32 | func (p ParseError) Error() string { 33 | return errMsgParseFailure 34 | } 35 | 36 | // Parse produces an IrcMessage from a byte slice. The string is an irc 37 | // protocol message, split by \r\n, and \r\n should not be 38 | // present at the end of the string. 39 | func Parse(str []byte) (*irc.Event, error) { 40 | parts := ircRegex.FindSubmatch(str) 41 | if parts == nil { 42 | return nil, ParseError{Irc: string(str)} 43 | } 44 | 45 | sender := string(parts[1]) 46 | name := string(parts[2]) 47 | var args []string 48 | if len(parts[3]) != 0 { 49 | args = strings.Fields(string(parts[3])) 50 | } 51 | 52 | if len(parts[4]) != 0 { 53 | if args != nil { 54 | args = append(args, string(parts[4])) 55 | } else { 56 | args = []string{string(parts[4])} 57 | } 58 | } 59 | 60 | return irc.NewEvent("", nil, name, sender, args...), nil 61 | } 62 | -------------------------------------------------------------------------------- /data/user_modes.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/aarondl/ultimateq/api" 5 | ) 6 | 7 | // UserModes provides basic modes for users. 8 | type UserModes struct { 9 | Modes byte `json:"modes"` 10 | ModeKinds *modeKinds `json:"mode_kinds"` 11 | } 12 | 13 | // NewUserModes creates a new usermodes using the metadata instance for 14 | // reference information. 15 | func NewUserModes(m *modeKinds) UserModes { 16 | return UserModes{ 17 | ModeKinds: m, 18 | } 19 | } 20 | 21 | // SetMode sets the mode given. 22 | func (u *UserModes) SetMode(mode rune) { 23 | u.Modes |= u.ModeKinds.modeBit(mode) 24 | } 25 | 26 | // HasMode checks if the user has the given mode. 27 | func (u *UserModes) HasMode(mode rune) bool { 28 | bit := u.ModeKinds.modeBit(mode) 29 | return bit != 0 && (bit == u.Modes&bit) 30 | } 31 | 32 | // UnsetMode unsets the mode given. 33 | func (u *UserModes) UnsetMode(mode rune) { 34 | u.Modes &= ^u.ModeKinds.modeBit(mode) 35 | } 36 | 37 | // String turns user modes into a string. 38 | func (u *UserModes) String() string { 39 | ret := "" 40 | for i := 0; i < len(u.ModeKinds.userPrefixes); i++ { 41 | if u.HasMode(u.ModeKinds.userPrefixes[i][0]) { 42 | ret += string(u.ModeKinds.userPrefixes[i][0]) 43 | } 44 | } 45 | return ret 46 | } 47 | 48 | // StringSymbols turns user modes into a string but uses mode chars instead. 49 | func (u *UserModes) StringSymbols() string { 50 | ret := "" 51 | for i := 0; i < len(u.ModeKinds.userPrefixes); i++ { 52 | if u.HasMode(u.ModeKinds.userPrefixes[i][0]) { 53 | ret += string(u.ModeKinds.userPrefixes[i][1]) 54 | } 55 | } 56 | return ret 57 | } 58 | 59 | // ToProto converts user modes into an api object 60 | func (u *UserModes) ToProto() *api.UserModes { 61 | um := new(api.UserModes) 62 | um.Modes = int32(u.Modes) 63 | um.Kinds = u.ModeKinds.ToProto() 64 | 65 | return um 66 | } 67 | -------------------------------------------------------------------------------- /registrar/holder.go: -------------------------------------------------------------------------------- 1 | package registrar 2 | 3 | import ( 4 | "github.com/aarondl/ultimateq/dispatch" 5 | "github.com/aarondl/ultimateq/dispatch/cmd" 6 | ) 7 | 8 | // holder stores registrations to an underlying Registrar and has the ability 9 | // to unregister everything. 10 | type holder struct { 11 | registrar Interface 12 | 13 | events map[uint64]struct{} 14 | commands map[uint64]struct{} 15 | } 16 | 17 | func newHolder(registrar Interface) *holder { 18 | h := &holder{ 19 | registrar: registrar, 20 | } 21 | h.initMaps() 22 | 23 | return h 24 | } 25 | 26 | func (h *holder) initMaps() { 27 | h.events = make(map[uint64]struct{}) 28 | h.commands = make(map[uint64]struct{}) 29 | } 30 | 31 | // Register and save the token we get back. 32 | func (h *holder) Register(network, channel, event string, handler dispatch.Handler) uint64 { 33 | id := h.registrar.Register(network, channel, event, handler) 34 | h.events[id] = struct{}{} 35 | 36 | return id 37 | } 38 | 39 | // RegisterCmd and save the names we use for later 40 | func (h *holder) RegisterCmd(network, channel string, command *cmd.Command) (uint64, error) { 41 | id, err := h.registrar.RegisterCmd(network, channel, command) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | h.commands[id] = struct{}{} 47 | 48 | return id, nil 49 | } 50 | 51 | // Unregister and discard our token that matches. 52 | func (h *holder) Unregister(id uint64) bool { 53 | if did := h.registrar.Unregister(id); !did { 54 | return false 55 | } 56 | 57 | delete(h.events, id) 58 | return true 59 | } 60 | 61 | // UnregisterCmd and discard our record of its registration. 62 | func (h *holder) UnregisterCmd(id uint64) bool { 63 | ok := h.registrar.UnregisterCmd(id) 64 | 65 | delete(h.commands, id) 66 | return ok 67 | } 68 | 69 | // unregisterAll applies unregister to all known registered things as well 70 | // as empties the maps. 71 | func (h *holder) unregisterAll() { 72 | for k := range h.events { 73 | h.registrar.Unregister(k) 74 | } 75 | 76 | for k := range h.commands { 77 | h.registrar.UnregisterCmd(k) 78 | } 79 | 80 | h.initMaps() 81 | } 82 | -------------------------------------------------------------------------------- /data/stored_channel_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestStoredChannel(t *testing.T) { 10 | t.Parallel() 11 | 12 | sc := NewStoredChannel("netID", "name") 13 | if sc == nil { 14 | t.Error("Failed creating new stored channel.") 15 | } 16 | 17 | if sc.NetID != "netID" || sc.Name != "name" { 18 | t.Error("Values not set correctly.") 19 | } 20 | 21 | if sc.JSONStorer == nil { 22 | t.Error("Did not initialize JSONStorer.") 23 | } 24 | } 25 | 26 | func TestStoredChannel_SerializeDeserialize(t *testing.T) { 27 | t.Parallel() 28 | 29 | netID, channel := "netID", "#bots" 30 | a := NewStoredChannel(netID, channel) 31 | 32 | serialized, err := a.serialize() 33 | if err != nil { 34 | t.Fatal("Unexpected error:", err) 35 | } 36 | if len(serialized) == 0 { 37 | t.Error("Serialization did not yield a serialized copy.") 38 | } 39 | 40 | b, err := deserializeChannel(serialized) 41 | if err != nil { 42 | t.Fatal("Deserialization failed.") 43 | } 44 | 45 | if a.Name != b.Name { 46 | t.Error("Name not deserlialize correctly.") 47 | } 48 | if a.NetID != b.NetID { 49 | t.Error("NetID not deserlialize correctly.") 50 | } 51 | } 52 | 53 | func TestStoredChannel_JSONify(t *testing.T) { 54 | t.Parallel() 55 | 56 | a := &StoredChannel{ 57 | NetID: "a", 58 | Name: "b", 59 | JSONStorer: JSONStorer{"some": "data"}, 60 | } 61 | var b StoredChannel 62 | 63 | str, err := json.Marshal(a) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | jsonStr := `{"netid":"a","name":"b","data":{"some":"data"}}` 69 | 70 | if string(str) != jsonStr { 71 | t.Errorf("Wrong JSON: %s", str) 72 | } 73 | 74 | if err = json.Unmarshal(str, &b); err != nil { 75 | t.Error(err) 76 | } 77 | 78 | if !reflect.DeepEqual(*a, b) { 79 | t.Error("A and B differ:", a, b) 80 | } 81 | } 82 | 83 | func TestStoredChannel_Protofy(t *testing.T) { 84 | t.Parallel() 85 | 86 | a := &StoredChannel{ 87 | NetID: "a", 88 | Name: "b", 89 | JSONStorer: JSONStorer{"some": "data"}, 90 | } 91 | var b StoredChannel 92 | 93 | b.FromProto(a.ToProto()) 94 | 95 | if !reflect.DeepEqual(*a, b) { 96 | t.Error("A and B differ:", a, b) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /parse/parse_test.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/aarondl/ultimateq/irc" 8 | ) 9 | 10 | func b(s string) []byte { 11 | return []byte(s) 12 | } 13 | 14 | type a []string 15 | 16 | func TestParse(t *testing.T) { 17 | sender := ":nick!user@host.com" 18 | testargs := []string{ 19 | "&channel1,#channel2", 20 | ":message1 message2", 21 | } 22 | 23 | wholeMsg := sender + " " + irc.PRIVMSG + " " + strings.Join(testargs, " ") 24 | noSender := irc.PRIVMSG + " " + strings.Join(testargs, " ") 25 | 26 | tests := []struct { 27 | Msg []byte 28 | Name string 29 | Sender string 30 | Args []string 31 | Error bool 32 | }{ 33 | {b(wholeMsg), irc.PRIVMSG, sender, testargs, false}, 34 | {b(noSender), irc.PRIVMSG, "", testargs, false}, 35 | {b(":irc PING :4005945"), irc.PING, "irc", a{"4005945"}, false}, 36 | {b(":irc PING 4005945 "), irc.PING, "irc", a{"4005945"}, false}, 37 | {b(":irc 005 nobody1 RFC2812 CHANLIMIT=#&:+20 :are supported"), 38 | irc.RPL_ISUPPORT, "irc", 39 | a{"nobody1", "RFC2812", "CHANLIMIT=#&:+20", "are supported"}, 40 | false, 41 | }, 42 | {b("irc fail message"), "", "", nil, true}, 43 | } 44 | 45 | for _, test := range tests { 46 | ev, err := Parse(test.Msg) 47 | 48 | if !test.Error && err != nil { 49 | t.Errorf("%s => Unexpected Error: %v", test.Msg, err) 50 | } else if test.Error && err == nil { 51 | t.Errorf("%s => Expected error but got nothing", test.Msg) 52 | } else { 53 | continue 54 | } 55 | 56 | if ev.Name != test.Name { 57 | t.Errorf("%s => Expected name: %v got %v", 58 | test.Msg, test.Name, ev.Name) 59 | } 60 | 61 | if ev.Sender != strings.TrimLeft(test.Sender, ":") { 62 | t.Errorf("%s => Expected sender: %v got %v", 63 | test.Msg, test.Sender[1:], ev.Sender) 64 | } 65 | 66 | if len(test.Args) != len(ev.Args) { 67 | t.Errorf("%s => Expected: %d arguments, got: %d", 68 | test.Msg, len(test.Args), len(ev.Args)) 69 | } 70 | 71 | for i, expectArg := range test.Args { 72 | expectArg = strings.TrimLeft(expectArg, ":") 73 | if ev.Args[i] != expectArg { 74 | t.Errorf("%s => Expected Arg[%d]: %s, got: %s", 75 | test.Msg, i, expectArg, ev.Args[i]) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /bot/extlocal_test.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "gopkg.in/inconshreveable/log15.v2" 8 | ) 9 | 10 | type testFakeExt struct { 11 | b *Bot 12 | 13 | handlerID int64 14 | } 15 | 16 | func (f *testFakeExt) Init(b *Bot) error { 17 | f.b = b 18 | return nil 19 | } 20 | 21 | func (f *testFakeExt) Deinit(b *Bot) error { 22 | f.b = nil 23 | return nil 24 | } 25 | 26 | func TestRegister(t *testing.T) { 27 | defer func() { 28 | extensions = make(map[string]Extension) 29 | }() 30 | 31 | fake := &testFakeExt{} 32 | RegisterExtension("fakeext", fake) 33 | 34 | if got := extensions["fakeext"]; got != fake { 35 | t.Errorf("Fake extension expected: (%T)#%v (%T)#%v", got, got, fake, fake) 36 | } 37 | } 38 | 39 | var errTestExtensionFailure = errors.New("ext error") 40 | 41 | type testFakeExtErr struct { 42 | } 43 | 44 | func (t *testFakeExtErr) Init(*Bot) error { 45 | return errTestExtensionFailure 46 | } 47 | func (t *testFakeExtErr) Deinit(*Bot) error { 48 | return errTestExtensionFailure 49 | } 50 | 51 | func TestBot_Extensions(t *testing.T) { 52 | defer func() { 53 | extensions = make(map[string]Extension) 54 | }() 55 | 56 | fake := &testFakeExt{} 57 | RegisterExtension("fakeext", fake) 58 | 59 | b := &Bot{} 60 | b.Logger = log15.New() 61 | b.Logger.SetHandler(log15.DiscardHandler()) 62 | if err := b.initLocalExtensions(); err != nil { 63 | t.Error(err) 64 | } 65 | 66 | if fake.b != b { 67 | t.Error("Expected bot to be passed to extension") 68 | } 69 | 70 | if err := b.deinitLocalExtensions(); err != nil { 71 | t.Error(err) 72 | } 73 | 74 | if fake.b != nil { 75 | t.Error("Expected bot to be erased by destructor") 76 | } 77 | } 78 | 79 | func TestBot_ExtensionsErrors(t *testing.T) { 80 | defer func() { 81 | extensions = make(map[string]Extension) 82 | }() 83 | 84 | fake := &testFakeExtErr{} 85 | RegisterExtension("fakeext", fake) 86 | 87 | b := &Bot{} 88 | b.Logger = log15.New() 89 | b.Logger.SetHandler(log15.DiscardHandler()) 90 | 91 | if err := b.initLocalExtensions(); err == nil { 92 | t.Error("Expected an error about failure to init") 93 | } 94 | 95 | if err := b.deinitLocalExtensions(); err == nil { 96 | t.Error("Expected an error about failure to deinit") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /data/stored_channel.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/aarondl/ultimateq/api" 10 | ) 11 | 12 | // StoredChannel stores attributes for channels. 13 | type StoredChannel struct { 14 | NetID string `json:"netid"` 15 | Name string `json:"name"` 16 | JSONStorer `json:"data"` 17 | } 18 | 19 | // NewStoredChannel creates a new stored channel. 20 | func NewStoredChannel(netID, name string) *StoredChannel { 21 | return &StoredChannel{netID, name, make(JSONStorer)} 22 | } 23 | 24 | // Clone deep copies this StoredChannel. 25 | func (s *StoredChannel) Clone() *StoredChannel { 26 | return &StoredChannel{s.NetID, s.Name, s.JSONStorer.Clone()} 27 | } 28 | 29 | // makeID is used to create a key to store this instance by. 30 | func (s *StoredChannel) makeID() string { 31 | return strings.ToLower(fmt.Sprintf("%s.%s", s.Name, s.NetID)) 32 | } 33 | 34 | // serialize turns the StoredChannel into bytes for storage. 35 | func (s *StoredChannel) serialize() ([]byte, error) { 36 | buffer := &bytes.Buffer{} 37 | encoder := gob.NewEncoder(buffer) 38 | err := encoder.Encode(s) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return buffer.Bytes(), nil 44 | } 45 | 46 | // deserializeChannel reverses the Serialize process. 47 | func deserializeChannel(serialized []byte) (*StoredChannel, error) { 48 | buffer := &bytes.Buffer{} 49 | decoder := gob.NewDecoder(buffer) 50 | if _, err := buffer.Write(serialized); err != nil { 51 | return nil, err 52 | } 53 | 54 | dec := &StoredChannel{} 55 | err := decoder.Decode(dec) 56 | return dec, err 57 | } 58 | 59 | func (s *StoredChannel) ToProto() *api.StoredChannel { 60 | var proto api.StoredChannel 61 | 62 | proto.Net = s.NetID 63 | proto.Name = s.Name 64 | 65 | if len(s.JSONStorer) != 0 { 66 | proto.Data = make(map[string]string, len(s.JSONStorer)) 67 | for k, v := range s.JSONStorer { 68 | proto.Data[k] = v 69 | } 70 | } 71 | 72 | return &proto 73 | } 74 | 75 | func (s *StoredChannel) FromProto(proto *api.StoredChannel) { 76 | s.NetID = proto.Net 77 | s.Name = proto.Name 78 | 79 | if len(proto.Data) != 0 { 80 | s.JSONStorer = make(JSONStorer, len(proto.Data)) 81 | for k, v := range proto.Data { 82 | s.JSONStorer[k] = v 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /dispatch/cmd/event.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aarondl/ultimateq/data" 7 | "github.com/aarondl/ultimateq/irc" 8 | ) 9 | 10 | // Event represents the data about the event that occurred. The commander 11 | // fills the Event structure with information about the user and channel 12 | // involved. It also embeds the State and Store for easy access. 13 | // 14 | // Some parts of Event will be nil under certain circumstances so elements 15 | // within must be checked for nil, see each element's documentation 16 | // for further information. 17 | type Event struct { 18 | *irc.Event 19 | 20 | // User can be nil if the bot's State is disabled. 21 | User *data.User 22 | // StoredUser will be nil when there is no required access. 23 | StoredUser *data.StoredUser 24 | // UserChannelModes will be nil when the message was not sent to a channel. 25 | UserChannelModes *data.UserModes 26 | // Channel will be nil when the message was not sent to a channel. 27 | Channel *data.Channel 28 | // TargetChannel will not be nil when the command has the #channel 29 | // parameter. The parameter can still be nil when the channel is not known 30 | // to the bot. 31 | TargetChannel *data.Channel 32 | // TargetUsers is populated when the arguments contain a ~nick argument, and 33 | // as a byproduct of looking up authentication, when the arguments contain 34 | // a *user argument, and a nickname is passed instead of a *username. 35 | TargetUsers map[string]*data.User 36 | // TargetStoredUser is populated when the arguments contain a *user 37 | // argument. 38 | TargetStoredUsers map[string]*data.StoredUser 39 | // TargetVarUsers is populated when the arguments contain a ~nick... 40 | // argument. When a *user... parameter is used, it will be sparsely filled 41 | // whenever a user is requested by nickname not *username. 42 | TargetVarUsers []*data.User 43 | // TargetVarUsers is populated when the arguments contain a *user... 44 | // argument. 45 | TargetVarStoredUsers []*data.StoredUser 46 | 47 | Args map[string]string 48 | } 49 | 50 | // SplitArg behaves exactly like GetArg but calls strings.Fields on the 51 | // argument. Useful for varargs... 52 | func (ev *Event) SplitArg(arg string) (args []string) { 53 | if str, ok := ev.Args[arg]; ok && len(str) > 0 { 54 | args = strings.Fields(str) 55 | } 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /mocks/conn_mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package mocks includes mocks to simplify testing. 3 | */ 4 | package mocks 5 | 6 | import ( 7 | "io" 8 | "net" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const ( 14 | panicMsg = "This function is not properly mocked." 15 | ) 16 | 17 | type IOReturn struct { 18 | n int 19 | err error 20 | } 21 | 22 | // Mock of Conn interface 23 | type Conn struct { 24 | writechan chan []byte 25 | writereturn chan IOReturn 26 | readchan chan []byte 27 | readreturn chan IOReturn 28 | killread chan int 29 | deathWaiter sync.WaitGroup 30 | } 31 | 32 | func NewConn() (conn *Conn) { 33 | conn = &Conn{ 34 | writechan: make(chan []byte), 35 | writereturn: make(chan IOReturn), 36 | readchan: make(chan []byte), 37 | readreturn: make(chan IOReturn), 38 | killread: make(chan int, 1), 39 | } 40 | 41 | conn.deathWaiter.Add(1) 42 | return 43 | } 44 | 45 | func (m *Conn) Receive(n int, err error) []byte { 46 | read := <-m.writechan 47 | m.writereturn <- IOReturn{n, err} 48 | return read 49 | } 50 | 51 | func (m *Conn) Write(written []byte) (int, error) { 52 | m.writechan <- written 53 | ret := <-m.writereturn 54 | return ret.n, ret.err 55 | } 56 | 57 | func (m *Conn) Send(buffer []byte, n int, err error) { 58 | m.readchan <- buffer 59 | m.readreturn <- IOReturn{n, err} 60 | } 61 | 62 | func (m *Conn) Read(buffer []byte) (int, error) { 63 | select { 64 | case read := <-m.readchan: 65 | copy(buffer, read) 66 | ret := <-m.readreturn 67 | return ret.n, ret.err 68 | case <-m.killread: 69 | } 70 | return 0, io.EOF 71 | } 72 | 73 | func (m *Conn) ResetDeath() { 74 | m.killread = make(chan int, 1) 75 | m.deathWaiter.Add(1) 76 | } 77 | 78 | func (m *Conn) WaitForDeath() { 79 | m.deathWaiter.Wait() 80 | } 81 | 82 | func (m *Conn) Close() error { 83 | m.killread <- 0 84 | m.deathWaiter.Done() 85 | return nil 86 | } 87 | 88 | func (m *Conn) LocalAddr() net.Addr { 89 | panic(panicMsg) 90 | return nil 91 | } 92 | 93 | func (m *Conn) RemoteAddr() net.Addr { 94 | panic(panicMsg) 95 | return nil 96 | } 97 | 98 | func (m *Conn) SetDeadline(_param0 time.Time) error { 99 | panic(panicMsg) 100 | return nil 101 | } 102 | 103 | func (m *Conn) SetReadDeadline(_param0 time.Time) error { 104 | panic(panicMsg) 105 | return nil 106 | } 107 | 108 | func (m *Conn) SetWriteDeadline(_param0 time.Time) error { 109 | panic(panicMsg) 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /data/user_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestUser_Create(t *testing.T) { 10 | t.Parallel() 11 | 12 | u := NewUser("") 13 | if got := u; got != nil { 14 | t.Errorf("Expected: %v to be nil.", got) 15 | } 16 | 17 | u = NewUser("nick") 18 | if u == nil { 19 | t.Error("Unexpected nil.") 20 | } 21 | if exp, got := u.Nick(), "nick"; exp != got { 22 | t.Errorf("Expected: %v, got: %v", exp, got) 23 | } 24 | if exp, got := u.Host.String(), "nick"; exp != got { 25 | t.Errorf("Expected: %v, got: %v", exp, got) 26 | } 27 | 28 | u = NewUser("nick!user@host") 29 | if u == nil { 30 | t.Error("Unexpected nil.") 31 | } 32 | if exp, got := u.Nick(), "nick"; exp != got { 33 | t.Errorf("Expected: %v, got: %v", exp, got) 34 | } 35 | if exp, got := u.Username(), "user"; exp != got { 36 | t.Errorf("Expected: %v, got: %v", exp, got) 37 | } 38 | if exp, got := u.Hostname(), "host"; exp != got { 39 | t.Errorf("Expected: %v, got: %v", exp, got) 40 | } 41 | if exp, got := u.Host.String(), "nick!user@host"; exp != got { 42 | t.Errorf("Expected: %v, got: %v", exp, got) 43 | } 44 | } 45 | 46 | func TestUser_String(t *testing.T) { 47 | t.Parallel() 48 | 49 | u := NewUser("nick") 50 | str := fmt.Sprint(u) 51 | if exp, got := str, "nick"; exp != got { 52 | t.Errorf("Expected: %v, got: %v", exp, got) 53 | } 54 | 55 | u = NewUser("nick!user@host") 56 | str = fmt.Sprint(u) 57 | if exp, got := str, "nick nick!user@host"; exp != got { 58 | t.Errorf("Expected: %v, got: %v", exp, got) 59 | } 60 | 61 | u = NewUser("nick") 62 | u.Realname = "realname realname" 63 | str = fmt.Sprint(u) 64 | if exp, got := str, "nick realname realname"; exp != got { 65 | t.Errorf("Expected: %v, got: %v", exp, got) 66 | } 67 | 68 | u = NewUser("nick!user@host") 69 | u.Realname = "realname realname" 70 | str = fmt.Sprint(u) 71 | if exp, got := str, "nick nick!user@host realname realname"; exp != got { 72 | t.Errorf("Expected: %v, got: %v", exp, got) 73 | } 74 | } 75 | 76 | func TestUser_JSONify(t *testing.T) { 77 | t.Parallel() 78 | 79 | a := NewUser("fish!fish@fish") 80 | a.Realname = "Fish" 81 | var b User 82 | 83 | str, err := json.Marshal(a) 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | 88 | if string(str) != `{"host":"fish!fish@fish","realname":"Fish"}` { 89 | t.Errorf("Wrong JSON: %s", str) 90 | } 91 | 92 | if err = json.Unmarshal(str, &b); err != nil { 93 | t.Error(err) 94 | } 95 | 96 | if *a != b { 97 | t.Error("A and B differ:", a, b) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /dispatch/cmd/command_args_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | rgxCreator = strings.NewReplacer( 12 | `(`, `\(`, `)`, `\)`, `]`, `\]`, `[`, 13 | `\[`, `\`, `\\`, `/`, `\/`, `%v`, `.*`, 14 | `*`, `\*`, 15 | ) 16 | ) 17 | 18 | func TestArgs(t *testing.T) { 19 | t.Parallel() 20 | 21 | helper := func(args ...string) error { 22 | c := &Command{ 23 | Args: args, 24 | } 25 | 26 | return c.parseArgs() 27 | } 28 | 29 | chkStr := func(msg, pattern string) error { 30 | pattern = `^` + rgxCreator.Replace(pattern) + `$` 31 | match, err := regexp.MatchString(pattern, msg) 32 | if err != nil { 33 | return fmt.Errorf("Error making pattern: \n\t%s\n\t%s", msg, pattern) 34 | } 35 | if !match { 36 | return fmt.Errorf("Unexpected: \n\t%s\n\t%s", msg, pattern) 37 | } 38 | return nil 39 | } 40 | 41 | chkErr := func(err error, pattern string) error { 42 | if err == nil { 43 | return fmt.Errorf("Error was nil but expected: %s", pattern) 44 | } 45 | return chkStr(err.Error(), pattern) 46 | } 47 | 48 | var err error 49 | 50 | err = helper("!!!") 51 | err = chkErr(err, errFmtArgumentForm) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | 56 | err = helper("~#badarg") 57 | err = chkErr(err, errFmtArgumentForm) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | err = helper("#*badarg") 63 | err = chkErr(err, errFmtArgumentForm) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | err = helper("[opt]", "req") 69 | err = chkErr(err, errFmtArgumentOrderReq) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | 74 | err = helper("req...", "[opt]") 75 | err = chkErr(err, errFmtArgumentOrderOpt) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | 80 | err = helper("name", "[name]") 81 | err = chkErr(err, errFmtArgumentDupName) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | 86 | err = helper("vrgs...", "vrgs2...") 87 | err = chkErr(err, errFmtArgumentDupVargs) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | 92 | err = helper("[opt]", "#chan1") 93 | err = chkErr(err, errFmtArgumentOrderChan) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | 98 | err = helper("vargs...", "#chan1") 99 | err = chkErr(err, errFmtArgumentOrderChan) 100 | if err != nil { 101 | t.Error(err) 102 | } 103 | 104 | err = helper("req", "#chan1") 105 | err = chkErr(err, errFmtArgumentOrderChan) 106 | if err != nil { 107 | t.Error(err) 108 | } 109 | 110 | err = helper("#chan1", "#chan2") 111 | err = chkErr(err, errFmtArgumentDupChan) 112 | if err != nil { 113 | t.Error(err) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /irc/event_test.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | testArgs = []string{"#chan1", "#chan2"} 10 | testEv = NewEvent("", nil, "", "nick!user@host", 11 | strings.Join(testArgs, ",")) 12 | ) 13 | 14 | func TestIrcEvent_Hostnames(t *testing.T) { 15 | if "nick" != testEv.Nick() { 16 | t.Error("Should have nick as a nick, had:", testEv.Nick()) 17 | } 18 | if "user" != testEv.Username() { 19 | t.Error("Should have user as a user, had:", testEv.Username()) 20 | } 21 | if "host" != testEv.Hostname() { 22 | t.Error("Should have host as a host, had:", testEv.Hostname()) 23 | } 24 | 25 | n, u, h := testEv.SplitHost() 26 | if "nick" != n { 27 | t.Error("Should have nick as a nick, had:", testEv.Nick()) 28 | } 29 | if "user" != u { 30 | t.Error("Should have user as a user, had:", testEv.Username()) 31 | } 32 | if "host" != h { 33 | t.Error("Should have host as a host, had:", testEv.Hostname()) 34 | } 35 | } 36 | 37 | func TestIrcEvent_Timestamp(t *testing.T) { 38 | if 0 == testEv.Time.Unix() { 39 | t.Error("Expected the timestamp to be set.") 40 | } 41 | } 42 | 43 | func TestIrcEvent_SplitArgs(t *testing.T) { 44 | for i, v := range testEv.SplitArgs(0) { 45 | if v != testArgs[i] { 46 | t.Error("Expected split args to line up with testargs but index", 47 | i, "was:", testArgs[i]) 48 | } 49 | } 50 | } 51 | 52 | func TestEvent_Target(t *testing.T) { 53 | args := []string{"#chan", "msg arg"} 54 | privmsg := &Event{ 55 | Name: PRIVMSG, 56 | Args: args, 57 | Sender: "user@host.com", 58 | } 59 | 60 | if targ := privmsg.Target(); targ != args[0] { 61 | t.Error("Should give the target of the privmsg, got:", targ) 62 | } 63 | } 64 | 65 | func TestEvent_Message(t *testing.T) { 66 | args := []string{"#chan", "msg arg"} 67 | notice := &Event{ 68 | Name: NOTICE, 69 | Args: args, 70 | Sender: "user@host.com", 71 | } 72 | 73 | if msg := notice.Message(); msg != args[1] { 74 | t.Error("Should give the message of the notice, got:", msg) 75 | } 76 | } 77 | 78 | func TestEvent_IsTargetChan(t *testing.T) { 79 | args := []string{"#chan", "msg arg"} 80 | privmsg := NewEvent("", NewNetworkInfo(), PRIVMSG, "user@host.com", args...) 81 | 82 | if !privmsg.IsTargetChan() { 83 | t.Error("The target should be a channel!") 84 | } 85 | 86 | args = []string{"user", "msg arg"} 87 | notice := NewEvent("", NewNetworkInfo(), NOTICE, "user@host.com", args...) 88 | 89 | if notice.IsTargetChan() { 90 | t.Error("The target should not be a channel!") 91 | } 92 | } 93 | 94 | func TestEvent_String(t *testing.T) { 95 | ev := NewEvent("", nil, PRIVMSG, "n!u@h", "arg1", "arg2 with space") 96 | exp := ":n!u@h PRIVMSG arg1 :arg2 with space" 97 | if got := ev.String(); got != exp { 98 | t.Errorf(`Expected: "%v", got "%v"`, exp, got) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /inet/queue_test.go: -------------------------------------------------------------------------------- 1 | package inet 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestQueue(t *testing.T) { 9 | q := Queue{} 10 | if exp, got := q.length, 0; exp != got { 11 | t.Errorf("Expected: %v, got: %v", exp, got) 12 | } 13 | if got := q.front; got != nil { 14 | t.Errorf("Expected: %v to be nil.", got) 15 | } 16 | if got := q.back; got != nil { 17 | t.Errorf("Expected: %v to be nil.", got) 18 | } 19 | } 20 | 21 | func TestQueue_Queuing(t *testing.T) { 22 | test1 := []byte{1, 2, 3} 23 | test2 := []byte{4, 5, 6} 24 | 25 | q := Queue{} 26 | 27 | q.Enqueue(test1) 28 | q.Enqueue(test2) 29 | 30 | dq1 := q.Dequeue() 31 | if exp, got := bytes.Compare(test1, dq1), 0; exp != got { 32 | t.Errorf("Expected: %v, got: %v", exp, got) 33 | } 34 | dq2 := q.Dequeue() 35 | if exp, got := bytes.Compare(test2, dq2), 0; exp != got { 36 | t.Errorf("Expected: %v, got: %v", exp, got) 37 | } 38 | } 39 | 40 | func TestQueue_queue(t *testing.T) { 41 | test1 := []byte{1, 2, 3} 42 | test2 := []byte{4, 5, 6} 43 | 44 | q := Queue{} 45 | q.Enqueue(nil) // Should be consequenceless test cov 46 | q.Enqueue(test1) 47 | if exp, got := q.length, 1; exp != got { 48 | t.Errorf("Expected: %v, got: %v", exp, got) 49 | } 50 | if exp, got := q.front, q.back; exp != got { 51 | t.Errorf("Expected: %v, got: %v", exp, got) 52 | } 53 | q.Enqueue(test2) 54 | if exp, got := q.length, 2; exp != got { 55 | t.Errorf("Expected: %v, got: %v", exp, got) 56 | } 57 | if exp, got := q.front, q.back; exp == got { 58 | t.Errorf("Did not want: %v, got: %v", exp, got) 59 | } 60 | 61 | if exp, got := bytes.Compare(*q.front.data, test1), 0; exp != got { 62 | t.Errorf("Expected: %v, got: %v", exp, got) 63 | } 64 | if exp, got := bytes.Compare(*q.front.next.data, test2), 0; exp != got { 65 | t.Errorf("Expected: %v, got: %v", exp, got) 66 | } 67 | } 68 | 69 | func TestQueue_dequeue(t *testing.T) { 70 | test1 := []byte{1, 2, 3} 71 | test2 := []byte{4, 5, 6} 72 | 73 | q := Queue{} 74 | if got := q.Dequeue(); got != nil { 75 | t.Errorf("Expected: %v to be nil.", got) 76 | } 77 | 78 | q.Enqueue(test1) 79 | q.Enqueue(test2) 80 | 81 | if exp, got := q.front, q.back; exp == got { 82 | t.Errorf("Did not want: %v, got: %v", exp, got) 83 | } 84 | dq1 := q.Dequeue() 85 | if exp, got := bytes.Compare(test1, dq1), 0; exp != got { 86 | t.Errorf("Expected: %v, got: %v", exp, got) 87 | } 88 | if exp, got := q.front, q.back; exp != got { 89 | t.Errorf("Expected: %v, got: %v", exp, got) 90 | } 91 | dq2 := q.Dequeue() 92 | if exp, got := bytes.Compare(test2, dq2), 0; exp != got { 93 | t.Errorf("Expected: %v, got: %v", exp, got) 94 | } 95 | if got := q.front; got != nil { 96 | t.Errorf("Expected: %v to be nil.", got) 97 | } 98 | if got := q.back; got != nil { 99 | t.Errorf("Expected: %v to be nil.", got) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /config/map_helpers_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestMapHelpers_Mp(t *testing.T) { 9 | t.Parallel() 10 | 11 | mp := mp{ 12 | "m": mp{}, 13 | "a": []map[string]interface{}{}, 14 | } 15 | 16 | if mp.get("m") == nil { 17 | t.Error("Expected to find a map m.") 18 | } 19 | if mp.getArr("a") == nil { 20 | t.Error("Expected to find a array of maps a.") 21 | } 22 | 23 | mp = nil 24 | if mp.get("m") != nil { 25 | t.Error("Expected it to be nil.") 26 | } 27 | if mp.getArr("a") != nil { 28 | t.Error("Expected it to be nil.") 29 | } 30 | } 31 | 32 | func TestMapHelpers_MpEnsure(t *testing.T) { 33 | t.Parallel() 34 | 35 | var m mp = map[string]interface{}{} 36 | second := m.ensure("first").ensure("second") 37 | if second == nil { 38 | t.Error("Expected it to return the new map.") 39 | } 40 | if m["first"] == nil { 41 | t.Error("Expected first to be created.") 42 | } 43 | if m.get("first").get("second") == nil { 44 | t.Error("Expected second to be created.") 45 | } 46 | if m.ensure("first") == nil { 47 | t.Error("Expected to get an old map of type map[string]interface{}") 48 | } 49 | 50 | m["first"] = m 51 | if m.ensure("first") == nil { 52 | t.Error("Expected to get an old map of type mp.") 53 | } 54 | 55 | m["first"] = interface{}(5) 56 | if nil != m.ensure("first").ensure("second") { 57 | t.Error("Expected a bad type to break it.") 58 | } 59 | } 60 | 61 | func TestMapHelpers_BadTypes(t *testing.T) { 62 | t.Parallel() 63 | 64 | bad := map[string]interface{}{ 65 | "badstr": 5, 66 | "badbool": 5, 67 | "baduint": "5", 68 | "badfloat": true, 69 | "badarr": false, 70 | } 71 | ctx := &NetCTX{&sync.RWMutex{}, nil, bad} 72 | 73 | if _, ok := getStr(ctx, "badstr", false); ok { 74 | t.Error("Expected the bad type to return nothing.") 75 | } 76 | if _, ok := getBool(ctx, "badbool", false); ok { 77 | t.Error("Expected the bad type to return nothing.") 78 | } 79 | if _, ok := getUint(ctx, "baduint", false); ok { 80 | t.Error("Expected the bad type to return nothing.") 81 | } 82 | if _, ok := getFloat64(ctx, "badfloat", false); ok { 83 | t.Error("Expected the bad type to return nothing.") 84 | } 85 | if _, ok := getStrArr(ctx, "badarr", false); ok { 86 | t.Error("Expected the bad type to return nothing.") 87 | } 88 | } 89 | 90 | func TestMapHelpers_GetStrArrEdgeCases(t *testing.T) { 91 | t.Parallel() 92 | 93 | parent := map[string]interface{}{ 94 | "arr": []string{}, 95 | "int": []interface{}{}, 96 | } 97 | child := make(map[string]interface{}) 98 | 99 | ctx := &NetCTX{&sync.RWMutex{}, parent, child} 100 | if _, ok := getStrArr(ctx, "arr", true); ok { 101 | t.Error("Expected empty array to return nothing.") 102 | } 103 | if _, ok := getStrArr(ctx, "int", true); ok { 104 | t.Error("Expected empty array to return nothing.") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /dispatch/dispatcher_test.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "bytes" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/aarondl/ultimateq/irc" 9 | "gopkg.in/inconshreveable/log15.v2" 10 | ) 11 | 12 | type testPoint struct { 13 | irc.Helper 14 | } 15 | 16 | type testCallback func(w irc.Writer, ev *irc.Event) 17 | 18 | type testHandler struct { 19 | callback testCallback 20 | } 21 | 22 | func (handler testHandler) Handle(w irc.Writer, ev *irc.Event) { 23 | if handler.callback != nil { 24 | handler.callback(w, ev) 25 | } 26 | } 27 | 28 | func TestDispatcher(t *testing.T) { 29 | t.Parallel() 30 | d := NewDispatcher(NewCore(nil)) 31 | if d == nil || d.trie == nil { 32 | t.Error("Initialization failed.") 33 | } 34 | } 35 | 36 | func TestDispatcherRegistration(t *testing.T) { 37 | t.Parallel() 38 | d := NewDispatcher(NewCore(nil)) 39 | handler := testHandler{} 40 | 41 | id := d.Register("", "", irc.PRIVMSG, handler) 42 | if id == 0 { 43 | t.Error("It should have given back an id.") 44 | } 45 | id2 := d.Register("", "", irc.PRIVMSG, handler) 46 | if id == id2 { 47 | t.Error("It should not produce duplicate ids.") 48 | } 49 | if !d.Unregister(id) { 50 | t.Error("It should unregister via it's id") 51 | } 52 | if d.Unregister(id) { 53 | t.Error("It should not unregister the same event multiple times.") 54 | } 55 | } 56 | 57 | func TestDispatcherDispatch(t *testing.T) { 58 | t.Parallel() 59 | d := NewDispatcher(NewCore(nil)) 60 | 61 | var count int64 62 | handler := testHandler{callback: func(irc.Writer, *irc.Event) { 63 | atomic.AddInt64(&count, 1) 64 | }} 65 | 66 | id := d.Register("", "", irc.PRIVMSG, handler) 67 | if id == 0 { 68 | t.Error("It should have given back an id.") 69 | } 70 | id2 := d.Register("", "", irc.PRIVMSG, handler) 71 | if id == id2 { 72 | t.Error("It should not produce duplicate ids.") 73 | } 74 | 75 | ev := irc.NewEvent("network", irc.NewNetworkInfo(), irc.PRIVMSG, "server", "#chan", "hey guys") 76 | d.Dispatch(nil, ev) 77 | d.WaitForHandlers() 78 | 79 | if count != 2 { 80 | t.Error("want 2 calls on the handler, got:", count) 81 | } 82 | } 83 | 84 | func TestDispatcherPanic(t *testing.T) { 85 | buf := &bytes.Buffer{} 86 | logger := log15.New() 87 | logger.SetHandler(log15.StreamHandler(buf, log15.LogfmtFormat())) 88 | 89 | logCore := NewCore(logger) 90 | d := NewDispatcher(logCore) 91 | 92 | panicMsg := "dispatch panic" 93 | handler := testHandler{ 94 | func(w irc.Writer, ev *irc.Event) { 95 | panic(panicMsg) 96 | }, 97 | } 98 | 99 | d.Register("", "", "", handler) 100 | ev := irc.NewEvent("network", netInfo, "dispatcher", irc.PRIVMSG, "panic test") 101 | d.Dispatch(testPoint{irc.Helper{}}, ev) 102 | d.WaitForHandlers() 103 | 104 | logStr := buf.String() 105 | 106 | if logStr == "" { 107 | t.Error("Expected not empty log.") 108 | } 109 | 110 | logBytes := buf.Bytes() 111 | if !bytes.Contains(logBytes, []byte(panicMsg)) { 112 | t.Errorf("Log does not contain: %s\n%s", panicMsg, logBytes) 113 | } 114 | 115 | if !bytes.Contains(logBytes, []byte("dispatcher_test.go")) { 116 | t.Error("Does not contain a reference to file that panic'd") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /data/user_modes_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestUserModes(t *testing.T) { 10 | t.Parallel() 11 | 12 | m := NewUserModes(testKinds) 13 | if got, exp := m.HasMode('o'), false; exp != got { 14 | t.Errorf("Expected: %v, got: %v", exp, got) 15 | } 16 | if got, exp := m.HasMode('v'), false; exp != got { 17 | t.Errorf("Expected: %v, got: %v", exp, got) 18 | } 19 | 20 | m.SetMode('o') 21 | if got, exp := m.HasMode('o'), true; exp != got { 22 | t.Errorf("Expected: %v, got: %v", exp, got) 23 | } 24 | if got, exp := m.HasMode('v'), false; exp != got { 25 | t.Errorf("Expected: %v, got: %v", exp, got) 26 | } 27 | m.SetMode('v') 28 | if got, exp := m.HasMode('o'), true; exp != got { 29 | t.Errorf("Expected: %v, got: %v", exp, got) 30 | } 31 | if got, exp := m.HasMode('v'), true; exp != got { 32 | t.Errorf("Expected: %v, got: %v", exp, got) 33 | } 34 | 35 | m.UnsetMode('o') 36 | if got, exp := m.HasMode('o'), false; exp != got { 37 | t.Errorf("Expected: %v, got: %v", exp, got) 38 | } 39 | if got, exp := m.HasMode('v'), true; exp != got { 40 | t.Errorf("Expected: %v, got: %v", exp, got) 41 | } 42 | m.UnsetMode('v') 43 | if got, exp := m.HasMode('o'), false; exp != got { 44 | t.Errorf("Expected: %v, got: %v", exp, got) 45 | } 46 | if got, exp := m.HasMode('v'), false; exp != got { 47 | t.Errorf("Expected: %v, got: %v", exp, got) 48 | } 49 | } 50 | 51 | func TestUserModes_String(t *testing.T) { 52 | t.Parallel() 53 | 54 | m := NewUserModes(testKinds) 55 | if got, exp := m.String(), ""; exp != got { 56 | t.Errorf("Expected: %v, got: %v", exp, got) 57 | } 58 | if got, exp := m.StringSymbols(), ""; exp != got { 59 | t.Errorf("Expected: %v, got: %v", exp, got) 60 | } 61 | m.SetMode('o') 62 | if got, exp := m.String(), "o"; exp != got { 63 | t.Errorf("Expected: %v, got: %v", exp, got) 64 | } 65 | if got, exp := m.StringSymbols(), "@"; exp != got { 66 | t.Errorf("Expected: %v, got: %v", exp, got) 67 | } 68 | m.SetMode('v') 69 | if got, exp := m.String(), "ov"; exp != got { 70 | t.Errorf("Expected: %v, got: %v", exp, got) 71 | } 72 | if got, exp := m.StringSymbols(), "@+"; exp != got { 73 | t.Errorf("Expected: %v, got: %v", exp, got) 74 | } 75 | m.UnsetMode('o') 76 | if got, exp := m.String(), "v"; exp != got { 77 | t.Errorf("Expected: %v, got: %v", exp, got) 78 | } 79 | if got, exp := m.StringSymbols(), "+"; exp != got { 80 | t.Errorf("Expected: %v, got: %v", exp, got) 81 | } 82 | } 83 | 84 | func TestUserModes_JSONify(t *testing.T) { 85 | t.Parallel() 86 | 87 | a := NewUserModes(testKinds) 88 | a.SetMode('o') 89 | var b UserModes 90 | 91 | str, err := json.Marshal(a) 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | 96 | jsonStr := `{"modes":1,"mode_kinds":` + 97 | `{"user_prefixes":[["o","@"],["v","+"]],` + 98 | `"channel_modes":{"a":1,"b":4,"c":2,"d":3,"x":1,"y":1,"z":1}}}` 99 | 100 | if string(str) != jsonStr { 101 | t.Errorf("Wrong JSON: %s", str) 102 | } 103 | 104 | if err = json.Unmarshal(str, &b); err != nil { 105 | t.Error(err) 106 | } 107 | 108 | if !reflect.DeepEqual(a, b) { 109 | t.Error("A and B differ:", a, b) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /data/channel.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aarondl/ultimateq/api" 7 | "github.com/aarondl/ultimateq/irc" 8 | ) 9 | 10 | const ( 11 | // banMode is the universal irc mode for bans 12 | banMode = 'b' 13 | ) 14 | 15 | // Channel encapsulates all the data associated with a channel. 16 | type Channel struct { 17 | Name string `json:"name"` 18 | Topic string `json:"topic"` 19 | Modes ChannelModes `json:"channel_modes"` 20 | } 21 | 22 | // NewChannel instantiates a channel object. 23 | func NewChannel(name string, m *modeKinds) *Channel { 24 | if len(name) == 0 { 25 | return nil 26 | } 27 | 28 | return &Channel{ 29 | Name: name, 30 | Modes: NewChannelModes(m), 31 | } 32 | } 33 | 34 | // Clone deep copies this Channel. 35 | func (c *Channel) Clone() *Channel { 36 | return &Channel{c.Name, c.Topic, c.Modes.Clone()} 37 | } 38 | 39 | // IsBanned checks a host to see if it's banned. 40 | func (c *Channel) IsBanned(host irc.Host) bool { 41 | if !strings.ContainsAny(string(host), "!@") { 42 | host += "!@" 43 | } 44 | bans := c.Modes.Addresses(banMode) 45 | for i := 0; i < len(bans); i++ { 46 | if irc.Mask(bans[i]).Match(host) { 47 | return true 48 | } 49 | } 50 | 51 | return false 52 | } 53 | 54 | // SetBans sets the bans of the channel. 55 | func (c *Channel) SetBans(bans []string) { 56 | delete(c.Modes.modes, banMode) 57 | for i := 0; i < len(bans); i++ { 58 | c.Modes.setAddress(banMode, bans[i]) 59 | } 60 | } 61 | 62 | // AddBan adds to the channel's bans. 63 | func (c *Channel) AddBan(ban string) { 64 | c.Modes.setAddress(banMode, ban) 65 | } 66 | 67 | // Bans gets the bans of the channel. 68 | func (c *Channel) Bans() []string { 69 | getBans := c.Modes.Addresses(banMode) 70 | if getBans == nil { 71 | return nil 72 | } 73 | bans := make([]string, len(getBans)) 74 | copy(bans, getBans) 75 | return bans 76 | } 77 | 78 | // HasBan checks to see if a specific mask is present in the banlist. 79 | func (c *Channel) HasBan(ban string) bool { 80 | return c.Modes.isAddressSet(banMode, ban) 81 | } 82 | 83 | // DeleteBan deletes a ban from the list. 84 | func (c *Channel) DeleteBan(ban string) { 85 | c.Modes.unsetAddress(banMode, ban) 86 | } 87 | 88 | // String returns the name of the channel. 89 | func (c *Channel) String() string { 90 | return c.Name 91 | } 92 | 93 | // DeleteBans deletes all bans that match a mask. 94 | func (c *Channel) DeleteBans(mask irc.Host) { 95 | bans := c.Modes.Addresses(banMode) 96 | if 0 == len(bans) { 97 | return 98 | } 99 | 100 | if !strings.ContainsAny(string(mask), "!@") { 101 | mask += "!@" 102 | } 103 | 104 | toRemove := make([]string, 0, 1) // Assume only one ban will match. 105 | for i := 0; i < len(bans); i++ { 106 | if irc.Mask(bans[i]).Match(mask) { 107 | toRemove = append(toRemove, bans[i]) 108 | } 109 | } 110 | 111 | for i := 0; i < len(toRemove); i++ { 112 | c.Modes.unsetAddress(banMode, toRemove[i]) 113 | } 114 | } 115 | 116 | // ToProto converts to a protocol buffer 117 | func (c *Channel) ToProto() *api.StateChannel { 118 | ch := new(api.StateChannel) 119 | 120 | ch.Name = c.Name 121 | ch.Topic = c.Topic 122 | ch.Modes = c.Modes.ToProto() 123 | 124 | return ch 125 | } 126 | -------------------------------------------------------------------------------- /dispatch/dispatcher.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/aarondl/ultimateq/data" 7 | "github.com/aarondl/ultimateq/dispatch/cmd" 8 | 9 | "github.com/aarondl/ultimateq/irc" 10 | ) 11 | 12 | // Handler is the interface for use with normal dispatching 13 | type Handler interface { 14 | Handle(w irc.Writer, ev *irc.Event) 15 | } 16 | 17 | // HandlerFunc implements the Handler interface 18 | type HandlerFunc func(w irc.Writer, ev *irc.Event) 19 | 20 | // Handle implements Handler interface 21 | func (h HandlerFunc) Handle(w irc.Writer, ev *irc.Event) { 22 | h(w, ev) 23 | } 24 | 25 | // EventDispatcher dispatches simple events 26 | type EventDispatcher interface { 27 | Register(network, channel, event string, handler Handler) uint64 28 | Unregister(id uint64) bool 29 | Dispatch(w irc.Writer, ev *irc.Event) 30 | } 31 | 32 | // CmdDispatcher dispatches complex commands 33 | type CmdDispatcher interface { 34 | Register(network, channel string, command *cmd.Command) (uint64, error) 35 | Unregister(id uint64) bool 36 | Dispatch(irc.Writer, *irc.Event, data.Provider) (bool, error) 37 | } 38 | 39 | // Dispatcher is made for handling dispatching of raw-ish irc events. 40 | type Dispatcher struct { 41 | *Core 42 | 43 | trieMut sync.RWMutex 44 | trie *trie 45 | } 46 | 47 | // NewDispatcher initializes an empty dispatcher ready to register events. 48 | func NewDispatcher(core *Core) *Dispatcher { 49 | return &Dispatcher{ 50 | Core: core, 51 | trie: newTrie(false), 52 | } 53 | } 54 | 55 | // Register registers an event handler to a particular event. In return a 56 | // unique identifer is given to later pass into Unregister in case of a need 57 | // to unregister the event handler. Pass in an empty string to any of network, 58 | // channel or event to prevent filtering on that parameter. Panics if it's 59 | // given a type that doesn't implement any of the correct interfaces. 60 | func (d *Dispatcher) Register(network, channel, event string, handler Handler) uint64 { 61 | if event == irc.RAW { 62 | event = "" 63 | } 64 | d.trieMut.Lock() 65 | id := d.trie.register(network, channel, event, handler) 66 | d.trieMut.Unlock() 67 | 68 | return id 69 | } 70 | 71 | // Unregister uses the identifier returned by Register to unregister a 72 | // callback from the Dispatcher. If the callback was removed it returns 73 | // true, false if it could not be found. 74 | func (d *Dispatcher) Unregister(id uint64) bool { 75 | d.trieMut.Lock() 76 | did := d.trie.unregister(id) 77 | d.trieMut.Unlock() 78 | 79 | return did 80 | } 81 | 82 | // Dispatch an IrcMessage to event handlers handling event also ensures all raw 83 | // handlers receive all messages. 84 | func (d *Dispatcher) Dispatch(w irc.Writer, ev *irc.Event) { 85 | network := ev.NetworkID 86 | event := ev.Name 87 | var channel string 88 | if len(ev.Args) > 0 && ev.IsTargetChan() { 89 | channel = ev.Target() 90 | } 91 | 92 | d.trieMut.RLock() 93 | handlers := d.trie.handlers(network, channel, event) 94 | d.trieMut.RUnlock() 95 | 96 | for _, handler := range handlers { 97 | h := handler.(Handler) 98 | d.HandlerStarted() 99 | go func() { 100 | defer d.HandlerFinished() 101 | defer d.PanicHandler() 102 | h.Handle(w, ev) 103 | }() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /config/config_file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | 8 | "github.com/BurntSushi/toml" 9 | ) 10 | 11 | const ( 12 | // defaultConfigFileName specifies a config file name in the event that 13 | // none was given, but a write to the file is requested with no name given. 14 | defaultConfigFileName = "config.toml" 15 | // errMsgInvalidConfigFile is when the toml does not successfully parse 16 | errMsgInvalidConfigFile = "config: Failed to load config file (%v)" 17 | // errMsgFileError occurs if the file could not be opened. 18 | errMsgFileError = "config: Failed to open config file (%v)" 19 | ) 20 | 21 | type ( 22 | wrFileCallback func(string) (io.WriteCloser, error) 23 | roFileCallback func(string) (io.ReadCloser, error) 24 | ) 25 | 26 | // FromFile overwrites the current config with the contents of the file. 27 | // It will use defaultConfigFileName if filename is the empty string. 28 | func (c *Config) FromFile(filename string) *Config { 29 | provider := func(name string) (io.ReadCloser, error) { 30 | return os.Open(name) 31 | } 32 | 33 | c.fromFile(filename, provider) 34 | return c 35 | } 36 | 37 | // fromFile reads the file provided by the callback and turns it 38 | // into a config. The file provided is closed by this function. It overrides 39 | // filename with defaultConfigFileName if it's the empty string. 40 | func (c *Config) fromFile(filename string, fn roFileCallback) *Config { 41 | if filename == "" { 42 | filename = defaultConfigFileName 43 | } 44 | 45 | file, err := fn(filename) 46 | if err != nil { 47 | c.addError(errMsgFileError, err) 48 | } else { 49 | defer file.Close() 50 | c.FromReader(file) 51 | 52 | c.protect.Lock() 53 | defer c.protect.Unlock() 54 | c.filename = filename 55 | } 56 | 57 | return c 58 | } 59 | 60 | // FromString overwrites the current config with the contents of the string. 61 | func (c *Config) FromString(config string) *Config { 62 | buf := bytes.NewBufferString(config) 63 | c.FromReader(buf) 64 | return c 65 | } 66 | 67 | // FromReader overwrites the current config with the contents of the reader. 68 | func (c *Config) FromReader(reader io.Reader) *Config { 69 | c.protect.Lock() 70 | defer c.protect.Unlock() 71 | c.clear() 72 | 73 | _, err := toml.DecodeReader(reader, &c.values) 74 | if err != nil { 75 | c.addError(errMsgInvalidConfigFile, err) 76 | } 77 | 78 | return c 79 | } 80 | 81 | // ToFile writes a config out to a writer. If the filename is empty 82 | // it will write to the file that this config was loaded from, or it will 83 | // write to the defaultConfigFileName. 84 | func (c *Config) ToFile(filename string) error { 85 | provider := func(f string) (io.WriteCloser, error) { 86 | return os.Create(filename) 87 | } 88 | 89 | return c.toFile(filename, provider) 90 | } 91 | 92 | // toFile uses a callback to get a ReadWriter to write to. It also 93 | // manages resolving the filename properly and writing the config to the Writer. 94 | // The file provided by the callback is closed in this function. 95 | func (c *Config) toFile(filename string, getFile wrFileCallback) error { 96 | if filename == "" { 97 | filename = c.Filename() 98 | } 99 | 100 | writer, err := getFile(filename) 101 | if err != nil { 102 | return err 103 | } 104 | defer writer.Close() 105 | 106 | return c.ToWriter(writer) 107 | } 108 | 109 | // ToWriter writes a config out to a writer. 110 | func (c *Config) ToWriter(writer io.Writer) error { 111 | c.protect.RLock() 112 | defer c.protect.RUnlock() 113 | 114 | encoder := toml.NewEncoder(writer) 115 | err := encoder.Encode(c.values) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return err 121 | } 122 | -------------------------------------------------------------------------------- /bot/botconfig.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aarondl/ultimateq/config" 7 | "github.com/aarondl/ultimateq/irc" 8 | ) 9 | 10 | const ( 11 | // errFmtNewServer occurs when a server that was created by RehashConfig 12 | // fails to initialize for some reason. 13 | errFmtNewServer = "bot: The new server (%v) could not be instantiated: %v" 14 | ) 15 | 16 | type configCallback func(*config.Config) 17 | 18 | // ReadConfig opens the config for reading, for the duration of the callback 19 | // the config is synchronized. 20 | func (b *Bot) ReadConfig(fn configCallback) { 21 | fn(b.conf) 22 | } 23 | 24 | // WriteConfig opens the config for writing, for the duration of the callback 25 | // the config is synchronized. 26 | func (b *Bot) WriteConfig(fn configCallback) { 27 | fn(b.conf) 28 | } 29 | 30 | // ReplaceConfig replaces the current configuration for the bot. Running 31 | // servers not present in the new config will be shut down immediately, while 32 | // new servers will be connected to and started. Updates updateable attributes 33 | // from the new configuration for each server. Returns false if the config 34 | // had an error. 35 | func (b *Bot) ReplaceConfig(newConfig *config.Config) bool { 36 | if !newConfig.Validate() { 37 | return false 38 | } 39 | 40 | b.protectServers.Lock() 41 | defer b.protectServers.Unlock() // LIFO 42 | 43 | b.startNewServers(newConfig) 44 | 45 | for netID, s := range b.servers { 46 | if serverConf := newConfig.Network(netID); nil == serverConf { 47 | b.stopServer(s) 48 | delete(b.servers, netID) 49 | continue 50 | } else { 51 | s.rehashConfig(newConfig) 52 | } 53 | } 54 | 55 | b.conf.Replace(newConfig) 56 | return true 57 | } 58 | 59 | // startNewServers adds non-existing servers to the bot and starts them. 60 | func (b *Bot) startNewServers(newConfig *config.Config) { 61 | for _, net := range newConfig.Networks() { 62 | if serverConf := b.conf.Network(net); nil == serverConf { 63 | server, err := b.createServer(net, newConfig) 64 | if err != nil { 65 | b.botEnd <- fmt.Errorf(errFmtNewServer, net, err) 66 | continue 67 | } 68 | b.servers[net] = server 69 | 70 | go b.startServer(server, true, true) 71 | } 72 | } 73 | } 74 | 75 | // rehashConfig updates the server's config values from the new configuration. 76 | func (s *Server) rehashConfig(newConfig *config.Config) { 77 | oldNick, _ := s.conf.Network(s.networkID).Nick() 78 | newNick, _ := newConfig.Network(s.networkID).Nick() 79 | setNick := newNick != oldNick 80 | 81 | if setNick { 82 | s.Write([]byte(irc.NICK + " :" + newNick)) 83 | } 84 | } 85 | 86 | // Rehash loads the config from a file. It attempts to use the previously read 87 | // config file name if loaded from a file... If not it will use a default file 88 | // name. It then calls Bot.ReplaceConfig. 89 | func (b *Bot) Rehash() error { 90 | fname, _ := b.conf.StoreFile() 91 | 92 | conf := config.New().FromFile(fname) 93 | if !CheckConfig(conf) { 94 | return errInvalidConfig 95 | } 96 | b.conf.Replace(conf) 97 | return nil 98 | } 99 | 100 | // DumpConfig dumps the config to a file. It attempts to use the previously read 101 | // config file name if loaded from a file... If not it will use a default file 102 | // name. 103 | func (b *Bot) DumpConfig() error { 104 | return b.conf.ToFile("") 105 | } 106 | 107 | // contains checks that the string arrays contain the same elements. 108 | func contains(a, b []string) bool { 109 | lena, lenb := len(a), len(b) 110 | if lena != lenb { 111 | return false 112 | } 113 | 114 | for i := 0; i < lena; i++ { 115 | j := 0 116 | for ; j < lenb; j++ { 117 | if a[i] == b[j] { 118 | break 119 | } 120 | } 121 | if j >= lenb { 122 | return false 123 | } 124 | } 125 | 126 | return true 127 | } 128 | -------------------------------------------------------------------------------- /data/channel_modes_diff.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // ModeDiff encapsulates a difference of modes, a combination of both positive 8 | // change modes, and negative change modes. 9 | type ModeDiff struct { 10 | pos ChannelModes 11 | neg ChannelModes 12 | 13 | *modeKinds 14 | } 15 | 16 | // NewModeDiff creates an empty ModeDiff. 17 | func NewModeDiff(m *modeKinds) ModeDiff { 18 | return ModeDiff{ 19 | modeKinds: m, 20 | pos: NewChannelModes(m), 21 | neg: NewChannelModes(m), 22 | } 23 | } 24 | 25 | // Clone deep copies the ModeDiff. 26 | func (d *ModeDiff) Clone() ModeDiff { 27 | return ModeDiff{ 28 | modeKinds: d.modeKinds, 29 | pos: d.pos.Clone(), 30 | neg: d.neg.Clone(), 31 | } 32 | } 33 | 34 | // IsSet checks if applying this diff will set the given simple modestrs. 35 | func (d *ModeDiff) IsSet(modestrs ...string) bool { 36 | return d.pos.IsSet(modestrs...) 37 | } 38 | 39 | // IsUnset checks if applying this diff will unset the given simple modestrs. 40 | func (d *ModeDiff) IsUnset(modestrs ...string) bool { 41 | return d.neg.IsSet(modestrs...) 42 | } 43 | 44 | // Apply takes a complex modestring and transforms it into a diff. 45 | // Assumes any modes not declared as part of ChannelModeKinds were not intended 46 | // for channel and are user-targeted (therefore taking an argument) 47 | // and returns them in two arrays, positive and negative modes respectively. 48 | func (d *ModeDiff) Apply(modestring string) ([]userMode, []userMode) { 49 | return apply(d, modestring) 50 | } 51 | 52 | // String turns a ModeDiff into a complex string representation. 53 | func (d *ModeDiff) String() string { 54 | modes := "" 55 | args := "" 56 | pos, neg := d.pos.String(), d.neg.String() 57 | if len(pos) > 0 { 58 | pspace := strings.IndexRune(pos, ' ') 59 | if pspace < 0 { 60 | pspace = len(pos) 61 | } else { 62 | args += " " + pos[pspace+1:] 63 | } 64 | modes += "+" + pos[:pspace] 65 | } 66 | if len(neg) > 0 { 67 | nspace := strings.IndexRune(neg, ' ') 68 | if nspace < 0 { 69 | nspace = len(neg) 70 | } else { 71 | args += " " + neg[nspace+1:] 72 | } 73 | modes += "-" + neg[:nspace] 74 | } 75 | 76 | return modes + args 77 | } 78 | 79 | // setMode adds this mode to the positive modes and removes it from the 80 | // negative modes. 81 | func (d *ModeDiff) setMode(mode rune) { 82 | d.pos.setMode(mode) 83 | d.neg.unsetMode(mode) 84 | } 85 | 86 | // unsetMode adds this mode to the negative modes and removes it from the 87 | // positive modes. 88 | func (d *ModeDiff) unsetMode(mode rune) { 89 | d.pos.unsetMode(mode) 90 | d.neg.setMode(mode) 91 | } 92 | 93 | // setArg adds this mode + argument to the positive modes and removes it 94 | // from the negative modes. 95 | func (d *ModeDiff) setArg(mode rune, arg string) { 96 | d.pos.setArg(mode, arg) 97 | d.neg.unsetArg(mode, arg) 98 | } 99 | 100 | // unsetArg adds this mode + argument to the negative modes and removes it 101 | // from the positive modes. 102 | func (d *ModeDiff) unsetArg(mode rune, arg string) { 103 | d.pos.unsetArg(mode, arg) 104 | d.neg.setArg(mode, arg) 105 | } 106 | 107 | // setAddress adds this mode + argument to the positive modes and removes it 108 | // from the negative modes. 109 | func (d *ModeDiff) setAddress(mode rune, address string) { 110 | d.pos.setAddress(mode, address) 111 | d.neg.unsetAddress(mode, address) 112 | } 113 | 114 | // unsetAddress adds this mode + argument to the negative modes and removes it 115 | // from the positive modes. 116 | func (d *ModeDiff) unsetAddress(mode rune, address string) { 117 | d.pos.unsetAddress(mode, address) 118 | d.neg.setAddress(mode, address) 119 | } 120 | 121 | // isUserMode checks if the given mode belongs to the user mode kinds. 122 | func (d ModeDiff) isUserMode(mode rune) (is bool) { 123 | if d.userPrefixes != nil { 124 | is = d.modeBit(mode) > 0 125 | } 126 | return 127 | } 128 | -------------------------------------------------------------------------------- /irc/event.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package irc defines types to be used by most other packages in 3 | the ultimateq system. It is small and comprised mostly of helper like types 4 | and constants. 5 | */ 6 | package irc 7 | 8 | import ( 9 | "bytes" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // Event contains all the information about an irc event. 15 | type Event struct { 16 | // Name of the event. Uppercase constant name or numeric. 17 | Name string `msgpack:"name"` 18 | // Sender is the server or user that sent the event, normally a fullhost. 19 | Sender string `msgpack:"sender"` 20 | // Args split by space delimiting. 21 | Args []string `msgpack:"args"` 22 | // Times is the time this event was received. 23 | Time time.Time `msgpack:"time"` 24 | // NetworkID is the ID of the network that sent this event. 25 | NetworkID string `msgpack:"network_id"` 26 | // NetworkInfo is the networks information. 27 | NetworkInfo *NetworkInfo `msgpack:"-"` 28 | } 29 | 30 | // NewEvent constructs a event object that has a timestamp. 31 | func NewEvent(netID string, ni *NetworkInfo, name, sender string, 32 | args ...string) *Event { 33 | 34 | var setArgs []string 35 | if len(args) > 0 { 36 | setArgs = make([]string, len(args)) 37 | copy(setArgs, args) 38 | } 39 | return &Event{name, sender, setArgs, time.Now().UTC(), netID, ni} 40 | } 41 | 42 | // Nick returns the nick of the sender. Will be empty string if it was 43 | // not able to parse the sender. 44 | func (e *Event) Nick() string { 45 | return Nick(e.Sender) 46 | } 47 | 48 | // Username returns the username of the sender. Will be empty string if it was 49 | // not able to parse the sender. 50 | func (e *Event) Username() string { 51 | return Username(e.Sender) 52 | } 53 | 54 | // Hostname returns the host of the sender. Will be empty string if it was 55 | // not able to parse the sender. 56 | func (e *Event) Hostname() string { 57 | return Hostname(e.Sender) 58 | } 59 | 60 | // SplitHost splits the sender into it's fragments: nick, user, and hostname. 61 | // If the format is not acceptable empty string is returned for everything. 62 | func (e *Event) SplitHost() (nick, user, hostname string) { 63 | return Split(e.Sender) 64 | } 65 | 66 | // SplitArgs splits string arguments. A convenience method to avoid having to 67 | // call splits and import strings. 68 | func (e *Event) SplitArgs(index int) []string { 69 | return strings.Split(e.Args[index], ",") 70 | } 71 | 72 | // Target retrieves the channel or user this event was sent to. Before using 73 | // this method it would be prudent to check that the Event.Name is a message 74 | // that supports a Target argument. 75 | func (e *Event) Target() string { 76 | return e.Args[0] 77 | } 78 | 79 | // IsTargetChan uses the underlying NetworkInfo to decide if this is a channel 80 | // or not. If there is no NetworkInfo it will panic. 81 | func (e *Event) IsTargetChan() bool { 82 | return e.NetworkInfo.IsChannel(e.Args[0]) 83 | } 84 | 85 | // Message retrieves the message sent to the user or channel. Before using 86 | // this method it would be prudent to check that the Event.Name is a message 87 | // that supports a Message argument. 88 | func (e *Event) Message() string { 89 | return e.Args[1] 90 | } 91 | 92 | // String turns this back into an IRC style message. 93 | func (e *Event) String() string { 94 | b := &bytes.Buffer{} 95 | if len(e.Sender) > 0 { 96 | b.WriteByte(':') 97 | b.WriteString(e.Sender) 98 | b.WriteByte(' ') 99 | } 100 | b.WriteString(e.Name) 101 | 102 | lastArg := len(e.Args) - 1 103 | for i, arg := range e.Args { 104 | b.WriteByte(' ') 105 | if lastArg == i && strings.ContainsRune(arg, ' ') { 106 | b.WriteByte(':') 107 | } 108 | b.WriteString(arg) 109 | } 110 | 111 | return b.String() 112 | } 113 | 114 | // IsCTCP checks if this event is a CTCP event. This means it's delimited 115 | // by the CTCPDelim as well as being PRIVMSG or NOTICE only. 116 | func (e *Event) IsCTCP() bool { 117 | return (e.Name == PRIVMSG || e.Name == NOTICE) && len(e.Args) >= 2 && 118 | IsCTCPString(e.Args[1]) 119 | } 120 | 121 | // UnpackCTCP can be called to retrieve a tag and data from a CTCP event. 122 | func (e *Event) UnpackCTCP() (tag, data string) { 123 | return CTCPunpackString(e.Args[1]) 124 | } 125 | -------------------------------------------------------------------------------- /bot/core_handler.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | 8 | "github.com/aarondl/ultimateq/config" 9 | "github.com/aarondl/ultimateq/irc" 10 | ) 11 | 12 | // coreHandler is the bot's main handling struct. As such it has access directly 13 | // to the bot itself. It's used to deal with mission critical events such as 14 | // pings, connects, disconnects etc. 15 | type coreHandler struct { 16 | // The bot this core handler belongs to. 17 | bot *Bot 18 | 19 | // How many nicks have been sent. 20 | nickvalue int 21 | untilJoinScale time.Duration 22 | 23 | // Protect access to core Handler 24 | protect sync.RWMutex 25 | } 26 | 27 | // HandleRaw implements the dispatch.EventHandler interface so the bot can 28 | // deal with all irc messages coming in. 29 | func (c *coreHandler) Handle(w irc.Writer, ev *irc.Event) { 30 | switch ev.Name { 31 | 32 | case irc.PING: 33 | w.Send(irc.PONG + " :" + ev.Args[0]) 34 | 35 | case irc.CONNECT: 36 | server := c.getServer(ev.NetworkID) 37 | 38 | c.protect.Lock() 39 | c.nickvalue = 0 40 | c.protect.Unlock() 41 | 42 | cfg := server.conf.Network(ev.NetworkID) 43 | nick, _ := cfg.Nick() 44 | uname, _ := cfg.Username() 45 | realname, _ := cfg.Realname() 46 | noautojoin, _ := cfg.NoAutoJoin() 47 | joindelay, _ := cfg.JoinDelay() 48 | 49 | if password, ok := cfg.Password(); ok { 50 | w.Send("PASS :", password) 51 | } 52 | 53 | w.Send("NICK :", nick) 54 | w.Sendf("USER %s 0 * :%s", uname, realname) 55 | 56 | if noautojoin { 57 | break 58 | } 59 | 60 | if chs, ok := cfg.Channels(); ok { 61 | <-time.After(c.untilJoinScale * time.Duration(joindelay)) 62 | for name, ch := range chs { 63 | if len(ch.Password) > 0 { 64 | w.Sendf("JOIN %s %s", name, ch.Password) 65 | } else { 66 | w.Sendf("JOIN %s", name) 67 | } 68 | } 69 | } 70 | case irc.KICK, irc.ERR_BANNEDFROMCHAN: 71 | server := c.getServer(ev.NetworkID) 72 | cfg := server.conf.Network(ev.NetworkID) 73 | noautojoin, _ := cfg.NoAutoJoin() 74 | joindelay, _ := cfg.JoinDelay() 75 | 76 | if noautojoin { 77 | break 78 | } 79 | 80 | var chs map[string]config.Channel 81 | var ok bool 82 | if chs, ok = cfg.Channels(); !ok { 83 | break 84 | } 85 | 86 | var nick, channel, curNick string 87 | if ev.Name == irc.KICK { 88 | channel = strings.ToLower(ev.Args[0]) 89 | nick = strings.ToLower(ev.Args[1]) 90 | } else { 91 | nick = strings.ToLower(ev.Args[0]) 92 | channel = strings.ToLower(ev.Args[1]) 93 | } 94 | 95 | curNick = strings.ToLower(c.bot.State(ev.NetworkID).Self().Nick()) 96 | 97 | if len(curNick) == 0 || nick != curNick { 98 | break 99 | } 100 | 101 | for name, ch := range chs { 102 | if strings.ToLower(name) != channel { 103 | continue 104 | } 105 | 106 | if ev.Name == irc.ERR_BANNEDFROMCHAN { 107 | <-time.After(c.untilJoinScale * time.Duration(joindelay)) 108 | } 109 | if len(ch.Password) > 0 { 110 | w.Sendf("JOIN %s %s", name, ch.Password) 111 | } else { 112 | w.Sendf("JOIN %s", name) 113 | } 114 | } 115 | 116 | case irc.ERR_NICKNAMEINUSE: 117 | server := c.getServer(ev.NetworkID) 118 | 119 | cfg := server.conf.Network(ev.NetworkID) 120 | nick, _ := cfg.Nick() 121 | altnick, _ := cfg.Altnick() 122 | 123 | c.protect.Lock() 124 | defer c.protect.Unlock() 125 | if c.nickvalue == 0 && len(altnick) > 0 { 126 | nick = altnick 127 | c.nickvalue++ 128 | } else { 129 | for i := 0; i < c.nickvalue; i++ { 130 | nick += "_" 131 | } 132 | c.nickvalue++ 133 | } 134 | w.Send("NICK :" + nick) 135 | 136 | case irc.JOIN: 137 | server := c.getServer(ev.NetworkID) 138 | if server.state != nil { 139 | if ev.Sender == server.state.Self().Host.String() { 140 | w.Send("WHO :", ev.Args[0]) 141 | w.Send("MODE :", ev.Args[0]) 142 | } 143 | } 144 | 145 | case irc.RPL_MYINFO: 146 | server := c.getServer(ev.NetworkID) 147 | server.netInfo.ParseMyInfo(ev) 148 | server.rehashNetworkInfo() 149 | 150 | case irc.RPL_ISUPPORT: 151 | server := c.getServer(ev.NetworkID) 152 | server.netInfo.ParseISupport(ev) 153 | server.rehashNetworkInfo() 154 | } 155 | } 156 | 157 | // getServer is a helper to look up the server based on w. 158 | func (c *coreHandler) getServer(netID string) *Server { 159 | c.bot.protectServers.RLock() 160 | defer c.bot.protectServers.RUnlock() 161 | return c.bot.servers[netID] 162 | } 163 | -------------------------------------------------------------------------------- /data/access.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/aarondl/ultimateq/api" 7 | ) 8 | 9 | const ( 10 | ascA = 65 11 | ascZ = 90 12 | asca = 97 13 | ascz = 122 14 | nAlphabet = 26 15 | none = "none" 16 | allFlags = `-ALL-` 17 | allFlagsNum uint64 = 0xFFFFFFFFFFFFF 18 | 19 | wholeAlphabet = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` 20 | ) 21 | 22 | // Access defines an access level and flags a-zA-Z for a user. 23 | type Access struct { 24 | Level uint8 `json:"level"` 25 | Flags uint64 `json:"flags"` 26 | } 27 | 28 | // NewAccess creates an access type with the permissions. 29 | func NewAccess(level uint8, flags ...string) *Access { 30 | a := &Access{} 31 | a.SetAccess(level, flags...) 32 | return a 33 | } 34 | 35 | // SetAccess sets all facets of access. 36 | func (a *Access) SetAccess(level uint8, flags ...string) { 37 | a.Level = level 38 | a.SetFlags(flags...) 39 | } 40 | 41 | // SetFlags sets many flags at once. 42 | func (a *Access) SetFlags(flags ...string) { 43 | a.Flags |= getFlagBits(flags...) 44 | } 45 | 46 | // ClearFlags clears many flags at once. 47 | func (a *Access) ClearFlags(flags ...string) { 48 | for _, flagset := range flags { 49 | for _, flag := range flagset { 50 | a.ClearFlag(flag) 51 | } 52 | } 53 | } 54 | 55 | // HasLevel checks to see that the level is >= the given level. 56 | func (a *Access) HasLevel(level uint8) bool { 57 | return a.Level >= level 58 | } 59 | 60 | // HasFlags checks many flags at once. Flags are or'd together. 61 | func (a *Access) HasFlags(flags ...string) bool { 62 | for _, flagset := range flags { 63 | for _, flag := range flagset { 64 | if a.HasFlag(flag) { 65 | return true 66 | } 67 | } 68 | } 69 | return false 70 | } 71 | 72 | // SetFlag sets the flag given. 73 | func (a *Access) SetFlag(flag rune) { 74 | a.Flags |= getFlagBit(flag) 75 | } 76 | 77 | // HasFlag checks if the user has the given flag. 78 | func (a *Access) HasFlag(flag rune) bool { 79 | bit := getFlagBit(flag) 80 | return bit != 0 && (bit == a.Flags&bit) 81 | } 82 | 83 | // ClearFlag clears the flag given. 84 | func (a *Access) ClearFlag(flag rune) { 85 | a.Flags &= ^getFlagBit(flag) 86 | } 87 | 88 | // ClearAllFlags clears all flags. 89 | func (a *Access) ClearAllFlags() { 90 | a.Flags = 0 91 | } 92 | 93 | // IsZero checks if this instance of access has no flags and no level. 94 | func (a *Access) IsZero() bool { 95 | return a.Flags == 0 && a.Level == 0 96 | } 97 | 98 | // String transforms the Access into a human-readable format. 99 | func (a Access) String() (str string) { 100 | hasLevel := a.Level != 0 101 | hasFlags := a.Flags != 0 102 | if !hasLevel && !hasFlags { 103 | return none 104 | } 105 | if hasLevel { 106 | str += strconv.Itoa(int(a.Level)) 107 | } 108 | if hasFlags { 109 | if hasLevel { 110 | str += " " 111 | } 112 | str += getFlagString(a.Flags) 113 | } 114 | 115 | return 116 | } 117 | 118 | // getFlagBits creates a mask containing all the modes. 119 | func getFlagBits(flags ...string) (bits uint64) { 120 | for _, flagset := range flags { 121 | for _, flag := range flagset { 122 | bits |= getFlagBit(flag) 123 | } 124 | } 125 | return 126 | } 127 | 128 | // getFlagBit maps A-Za-z to bits in a uint64 129 | func getFlagBit(flag rune) (bit uint64) { 130 | asc := uint64(flag) 131 | if asc >= ascA && asc <= ascZ { 132 | asc -= ascA 133 | bit = 1 << asc 134 | } else if asc >= asca && asc <= ascz { 135 | asc -= ascA + (asca - ascZ - 1) 136 | bit = 1 << asc 137 | } 138 | return 139 | } 140 | 141 | // getFlagString maps the bits in a uint64 to A-Za-z 142 | func getFlagString(bits uint64) (flags string) { 143 | var bit uint64 = 1 144 | var n = nAlphabet * 2 145 | 146 | if (bits & allFlagsNum) == allFlagsNum { 147 | return allFlags 148 | } 149 | 150 | for i := 0; i < n; i, bit = i+1, bit<<1 { 151 | if bit&bits != bit { 152 | continue 153 | } 154 | 155 | if i < nAlphabet { 156 | flags += string(i + ascA) 157 | } else { 158 | flags += string(i - nAlphabet + asca) 159 | } 160 | } 161 | return 162 | } 163 | 164 | func (a Access) ToProto() *api.Access { 165 | return &api.Access{ 166 | Level: uint32(a.Level), 167 | Flags: uint64(a.Flags), 168 | } 169 | } 170 | 171 | func (a *Access) FromProto(proto *api.Access) { 172 | a.Level = uint8(proto.Level) 173 | a.Flags = proto.Flags 174 | } 175 | -------------------------------------------------------------------------------- /irc/ctcp.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import "bytes" 4 | 5 | const ( 6 | ctcpDelim = '\x01' 7 | ctcpLowQuote = '\x10' 8 | ctcpHighQuote = '\x5C' 9 | ctcpSep = '\x20' 10 | ) 11 | 12 | // IsCTCP checks if the current byte string is a CTCP message. 13 | func IsCTCP(msg []byte) bool { 14 | return ctcpDelim == msg[0] && ctcpDelim == msg[len(msg)-1] 15 | } 16 | 17 | // IsCTCPString checks if the current string is a CTCP message. 18 | func IsCTCPString(msg string) bool { 19 | return ctcpDelim == msg[0] && ctcpDelim == msg[len(msg)-1] 20 | } 21 | 22 | // CTCPunpack unpacks a CTCP message. 23 | func CTCPunpack(msg []byte) (tag []byte, data []byte) { 24 | msg = msg[1 : len(msg)-1] 25 | 26 | msg = ctcpLowLevelUnescape(msg) 27 | tag, data = ctcpUnpack(msg) 28 | tag = ctcpHighLevelUnescape(tag) 29 | if data != nil { 30 | data = ctcpHighLevelUnescape(data) 31 | } 32 | return tag, data 33 | } 34 | 35 | // CTCPpack packs a message into CTCP format. 36 | func CTCPpack(tag, data []byte) []byte { 37 | if data != nil { 38 | data = ctcpHighLevelEscape(data) 39 | } 40 | tag = ctcpHighLevelEscape(tag) 41 | 42 | ret := ctcpPack(tag, data) 43 | ret = ctcpLowLevelEscape(ret) 44 | 45 | retDelimited := make([]byte, len(ret)+2) 46 | retDelimited[0] = ctcpDelim 47 | retDelimited[len(retDelimited)-1] = ctcpDelim 48 | copy(retDelimited[1:], ret) 49 | return retDelimited 50 | } 51 | 52 | // CTCPunpackString unpacks a CTCP message to strings. 53 | func CTCPunpackString(msg string) (tag, data string) { 54 | t, d := CTCPunpack([]byte(msg)) 55 | return string(t), string(d) 56 | } 57 | 58 | // CTCPpackString packs a message into CTCP format from strings. 59 | func CTCPpackString(tag, data string) string { 60 | ret := CTCPpack([]byte(tag), []byte(data)) 61 | return string(ret) 62 | } 63 | 64 | // ctcpUnpack extracts tagging data from the message data. 65 | // X-CHR ::= '\000' | '\002' .. '\377' 66 | // X-N-AS ::= '\000' | '\002' .. '\037' | '\041' .. '\377' 67 | // SPC ::= '\040' 68 | // X-MSG ::= | X-N-AS+ | X-N-AS+ SPC X-CHR* 69 | func ctcpUnpack(in []byte) ([]byte, []byte) { 70 | splits := bytes.SplitN(in, []byte{ctcpSep}, 2) 71 | 72 | if len(splits) == 2 { 73 | return splits[0], splits[1] 74 | } 75 | return splits[0], nil 76 | } 77 | 78 | // ctcpPack packs tagging data in with the message data. 79 | func ctcpPack(tag []byte, data []byte) []byte { 80 | if len(data) == 0 { 81 | return tag 82 | } 83 | 84 | ret := make([]byte, len(tag)+len(data)+1) 85 | copy(ret, tag) 86 | ret[len(tag)] = ctcpSep 87 | copy(ret[len(tag)+1:], data) 88 | return ret 89 | } 90 | 91 | // ctcpHighLevelEscape escapes the highest level of CTCP message. 92 | // X-DELIM ::= '\x01' 93 | // X-QUOTE ::= '\134' (0x5C) 94 | // X-DELIM --> X-QUOTE 'a' (0x61) 95 | // X-QUOTE --> X-QUOTE X-QUOTE 96 | func ctcpHighLevelEscape(in []byte) []byte { 97 | out := bytes.Replace(in, []byte{ctcpHighQuote}, 98 | []byte{ctcpHighQuote, ctcpHighQuote}, -1) 99 | out = bytes.Replace(out, []byte{0x01}, []byte{ctcpHighQuote, 0x61}, -1) 100 | return out 101 | } 102 | 103 | // ctcpHighLevelUnescape unescapes the ctcp message to get ready for the wire 104 | func ctcpHighLevelUnescape(in []byte) []byte { 105 | out := bytes.Replace(in, []byte{ctcpHighQuote, 0x61}, []byte{0x01}, -1) 106 | out = bytes.Replace(out, []byte{ctcpHighQuote, ctcpHighQuote}, 107 | []byte{ctcpHighQuote}, -1) 108 | return out 109 | } 110 | 111 | // ctcpLowLevelEscape escapes the low level of CTCP message. 112 | // M-QUOTE = M-QUOTE ::= '\xl0' 113 | // NUL --> M-QUOTE '0' 114 | // NL --> M-QUOTE 'n' 115 | // CR --> M-QUOTE 'r' 116 | // M-QUOTE --> M-QUOTE M-QUOTE 117 | func ctcpLowLevelEscape(in []byte) []byte { 118 | out := bytes.Replace(in, []byte{ctcpLowQuote}, 119 | []byte{ctcpLowQuote, ctcpLowQuote}, -1) 120 | out = bytes.Replace(out, []byte{'\r'}, []byte{ctcpLowQuote, '\r'}, -1) 121 | out = bytes.Replace(out, []byte{'\n'}, []byte{ctcpLowQuote, '\n'}, -1) 122 | out = bytes.Replace(out, []byte{0x00}, []byte{ctcpLowQuote, 0x00}, -1) 123 | return out 124 | } 125 | 126 | // ctcpLowLevelUnescape unescapes the ctcp message to get ready for the wire 127 | func ctcpLowLevelUnescape(in []byte) []byte { 128 | out := bytes.Replace(in, []byte{ctcpLowQuote, 0x00}, []byte{0x00}, -1) 129 | out = bytes.Replace(out, []byte{ctcpLowQuote, '\n'}, []byte{'\n'}, -1) 130 | out = bytes.Replace(out, []byte{ctcpLowQuote, '\r'}, []byte{'\r'}, -1) 131 | out = bytes.Replace(out, []byte{ctcpLowQuote, ctcpLowQuote}, 132 | []byte{ctcpLowQuote}, -1) 133 | return out 134 | } 135 | -------------------------------------------------------------------------------- /irc/hosts.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // rgxHost validates and splits hosts. 10 | rgxHost = regexp.MustCompile( 11 | `(?i)^` + 12 | `([\w\x5B-\x60][\w\d\x5B-\x60]*)` + // nickname 13 | `!([^\0@\s]+)` + // username 14 | `@([^\0\s]+)` + // host 15 | `$`, 16 | ) 17 | 18 | // rgxMask validates and splits masks. 19 | rgxMask = regexp.MustCompile( 20 | `(?i)^` + 21 | `([\w\x5B-\x60\?\*][\w\d\x5B-\x60\?\*]*)` + // nickname 22 | `!([^\0@\s]+)` + // username 23 | `@([^\0\s]+)` + // host 24 | `$`, 25 | ) 26 | ) 27 | 28 | // Host is a type that represents an irc hostname. nickname!username@hostname 29 | type Host string 30 | 31 | // Nick returns the nick of the host. 32 | func (h Host) Nick() string { 33 | return Nick(string(h)) 34 | } 35 | 36 | // Username returns the username of the host. 37 | func (h Host) Username() string { 38 | return Username(string(h)) 39 | } 40 | 41 | // Hostname returns the host of the host. 42 | func (h Host) Hostname() string { 43 | return Hostname(string(h)) 44 | } 45 | 46 | // Split splits a host into it's fragments: nick, user, and hostname. If the 47 | // format is not acceptable empty string is returned for everything. 48 | func (h Host) Split() (nick, user, hostname string) { 49 | return Split(string(h)) 50 | } 51 | 52 | // String returns the fullhost of this host. 53 | func (h Host) String() string { 54 | return string(h) 55 | } 56 | 57 | // IsValid checks to ensure the host is in valid format. 58 | func (h Host) IsValid() bool { 59 | return rgxHost.MatchString(string(h)) 60 | } 61 | 62 | // Mask is an irc hostmask that contains wildcard characters ? and * 63 | type Mask string 64 | 65 | // Match checks if the mask satisfies the given host. 66 | func (m Mask) Match(h Host) bool { 67 | return isMatch(string(h), string(m)) 68 | } 69 | 70 | // IsValid checks to ensure the mask is in valid format. 71 | func (m Mask) IsValid() bool { 72 | return rgxMask.MatchString(string(m)) 73 | } 74 | 75 | // Split splits a mask into it's fragments: nick, user, and host. If the 76 | // format is not acceptable empty string is returned for everything. 77 | func (m Mask) Split() (nick, user, host string) { 78 | fragments := rgxMask.FindStringSubmatch(string(m)) 79 | if len(fragments) == 0 { 80 | return 81 | } 82 | return fragments[1], fragments[2], fragments[3] 83 | } 84 | 85 | // Match checks if a given mask is satisfied by the host. 86 | func (h Host) Match(m Mask) bool { 87 | return isMatch(string(h), string(m)) 88 | } 89 | 90 | // isMatch is a matching function for a string, and a string with the wildcards 91 | // * and ? in it. 92 | func isMatch(hs, ms string) bool { 93 | ml, hl := len(ms), len(hs) 94 | 95 | if ml == 0 { 96 | return hl == 0 97 | } 98 | 99 | var i, j, consume = 0, 0, 0 100 | for i < ml && j < hl { 101 | 102 | switch ms[i] { 103 | case '?', '*': 104 | star := false 105 | consume = 0 106 | 107 | for i < ml && (ms[i] == '*' || ms[i] == '?') { 108 | star = star || ms[i] == '*' 109 | i++ 110 | consume++ 111 | } 112 | 113 | if star { 114 | consume = -1 115 | } 116 | case hs[j]: 117 | consume = 0 118 | i++ 119 | j++ 120 | default: 121 | if consume != 0 { 122 | consume-- 123 | j++ 124 | } else { 125 | return false 126 | } 127 | } 128 | } 129 | 130 | for i < ml && (ms[i] == '?' || ms[i] == '*') { 131 | i++ 132 | } 133 | 134 | if consume < 0 { 135 | consume = hl - j 136 | } 137 | j += consume 138 | 139 | if i < ml || j < hl { 140 | return false 141 | } 142 | 143 | return true 144 | } 145 | 146 | // Nick returns the nick of the host. 147 | func Nick(host string) string { 148 | index := strings.IndexAny(host, "!@") 149 | if index >= 0 { 150 | return host[:index] 151 | } 152 | return host 153 | } 154 | 155 | // Username returns the username of the host. 156 | func Username(host string) string { 157 | _, user, _ := Split(host) 158 | return user 159 | } 160 | 161 | // Hostname returns the host of the host. 162 | func Hostname(host string) string { 163 | _, _, hostname := Split(host) 164 | return hostname 165 | } 166 | 167 | // Split splits a host into it's fragments: nick, user, and hostname. If the 168 | // format is not acceptable empty string is returned for everything. 169 | func Split(host string) (nick, user, hostname string) { 170 | fragments := rgxHost.FindStringSubmatch(string(host)) 171 | if len(fragments) == 0 { 172 | return 173 | } 174 | return fragments[1], fragments[2], fragments[3] 175 | } 176 | -------------------------------------------------------------------------------- /irc/network_info_test.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | netID = "irc.gamesurge.net" 10 | 11 | _s0 = `NICK irc.test.net testircd-1.2 acCior abcde` 12 | 13 | _s1 = `NICK RFC8812 IRCD=gIRCd CASEMAPPING=scii PREFIX=(v)+ ` + 14 | `CHANTYPES=#& CHANMODES=a,b,c,d CHANLIMIT=#&+:10` 15 | 16 | _s2 = `NICK CHANNELLEN=49 NICKLEN=8 TOPICLEN=489 AWAYLEN=126 KICKLEN=399 ` + 17 | `MODES=4 MAXLIST=beI:49 EXCEPTS=e INVEX=I PENALTY` 18 | 19 | capsTest0 = &Event{ 20 | Name: RPL_MYINFO, 21 | Args: strings.Split(_s0, " "), 22 | Sender: netID, 23 | } 24 | capsTest1 = &Event{ 25 | Name: RPL_ISUPPORT, 26 | Args: append(strings.Split(_s1, " "), "are supported by this server"), 27 | Sender: netID, 28 | } 29 | capsTest2 = &Event{ 30 | Name: RPL_ISUPPORT, 31 | Args: append(strings.Split(_s2, " "), "are supported by this server"), 32 | Sender: netID, 33 | } 34 | ) 35 | 36 | func TestNetworkInfo_Parse(t *testing.T) { 37 | t.Parallel() 38 | p := NewNetworkInfo() 39 | 40 | p.ParseMyInfo(capsTest0) 41 | p.ParseISupport(capsTest1) 42 | p.ParseISupport(capsTest2) 43 | 44 | if exp, val := "irc.test.net", p.ServerName(); val != exp { 45 | t.Error("Unexpected:", val, "should be:", exp) 46 | } 47 | if exp, val := "testircd-1.2", p.IrcdVersion(); val != exp { 48 | t.Error("Unexpected:", val, "should be:", exp) 49 | } 50 | if exp, val := "acCior", p.Usermodes(); val != exp { 51 | t.Error("Unexpected:", val, "should be:", exp) 52 | } 53 | if exp, val := "abcde", p.LegacyChanmodes(); val != exp { 54 | t.Error("Unexpected:", val, "should be:", exp) 55 | } 56 | if exp, val := "RFC8812", p.RFC(); val != exp { 57 | t.Error("Unexpected:", val, "should be:", exp) 58 | } 59 | if exp, val := "gIRCd", p.IRCD(); val != exp { 60 | t.Error("Unexpected:", val, "should be:", exp) 61 | } 62 | if exp, val := "scii", p.Casemapping(); val != exp { 63 | t.Error("Unexpected:", val, "should be:", exp) 64 | } 65 | if exp, val := "(v)+", p.Prefix(); val != exp { 66 | t.Error("Unexpected:", val, "should be:", exp) 67 | } 68 | if exp, val := "#&", p.Chantypes(); val != exp { 69 | t.Error("Unexpected:", val, "should be:", exp) 70 | } 71 | if exp, val := "a,b,c,d", p.Chanmodes(); val != exp { 72 | t.Error("Unexpected:", val, "should be:", exp) 73 | } 74 | if exp, val := 10, p.Chanlimit(); val != exp { 75 | t.Error("Unexpected:", val, "should be:", exp) 76 | } 77 | if exp, val := 49, p.Channellen(); val != exp { 78 | t.Error("Unexpected:", val, "should be:", exp) 79 | } 80 | if exp, val := 8, p.Nicklen(); val != exp { 81 | t.Error("Unexpected:", val, "should be:", exp) 82 | } 83 | if exp, val := 489, p.Topiclen(); val != exp { 84 | t.Error("Unexpected:", val, "should be:", exp) 85 | } 86 | if exp, val := 126, p.Awaylen(); val != exp { 87 | t.Error("Unexpected:", val, "should be:", exp) 88 | } 89 | if exp, val := 399, p.Kicklen(); val != exp { 90 | t.Error("Unexpected:", val, "should be:", exp) 91 | } 92 | if exp, val := 4, p.Modes(); val != exp { 93 | t.Error("Unexpected:", val, "should be:", exp) 94 | } 95 | if exp, val := "e", p.Extra("EXCEPTS"); val != exp { 96 | t.Error("Unexpected:", val, "should be:", exp) 97 | } 98 | if exp, val := "true", p.Extra("PENALTY"); val != exp { 99 | t.Error("Unexpected:", val, "should be:", exp) 100 | } 101 | if exp, val := "I", p.Extra("INVEX"); val != exp { 102 | t.Error("Unexpected:", val, "should be:", exp) 103 | } 104 | if exp, val := "", p.Extra("NICK"); val != exp { 105 | t.Error("Unexpected:", val, "should be:", exp) 106 | } 107 | } 108 | 109 | func TestNetworkInfo_Clone(t *testing.T) { 110 | t.Parallel() 111 | other := "other" 112 | diff := "different" 113 | 114 | p1 := NewNetworkInfo() 115 | p1.extras[other] = other 116 | p2 := p1.Clone() 117 | p1.chantypes = other 118 | p1.extras[other] = diff 119 | 120 | if p2.chantypes == other { 121 | t.Error("Clones should not share memory.") 122 | } 123 | if p2.extras[other] != other { 124 | t.Error("The extras map should be deep copied.") 125 | } 126 | } 127 | 128 | func TestNetworkInfo_IsChannel(t *testing.T) { 129 | t.Parallel() 130 | p := NewNetworkInfo() 131 | p.chantypes = "#&~" 132 | if test := "#channel"; !p.IsChannel(test) { 133 | t.Error("Expected:", test, "to be a channel.") 134 | } 135 | if test := "&channel"; !p.IsChannel(test) { 136 | t.Error("Expected:", test, "to be a channel.") 137 | } 138 | if test := "n#otchannel"; p.IsChannel(test) { 139 | t.Error("Expected:", test, "to not be a channel.") 140 | } 141 | if p.IsChannel("") { 142 | t.Error("It should return false when empty.") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /registrar/holder_test.go: -------------------------------------------------------------------------------- 1 | package registrar 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/aarondl/ultimateq/dispatch" 8 | "github.com/aarondl/ultimateq/dispatch/cmd" 9 | ) 10 | 11 | type mockReg struct { 12 | regs int 13 | unregs int 14 | cmds int 15 | uncmds int 16 | 17 | id uint64 18 | err error 19 | ret bool 20 | } 21 | 22 | func (m *mockReg) Register(_, _, _ string, _ dispatch.Handler) uint64 { 23 | m.regs++ 24 | m.id++ 25 | return m.id 26 | } 27 | 28 | func (m *mockReg) RegisterCmd(_, _ string, _ *cmd.Command) (uint64, error) { 29 | m.cmds++ 30 | return 0, m.err 31 | } 32 | 33 | func (m *mockReg) Unregister(_ uint64) bool { 34 | m.unregs++ 35 | return m.ret 36 | } 37 | 38 | func (m *mockReg) UnregisterCmd(_ uint64) bool { 39 | m.uncmds++ 40 | return m.ret 41 | } 42 | 43 | func (m *mockReg) verifyMock(t *testing.T, regs, unregs, cmds, uncmds int) { 44 | t.Helper() 45 | if regs != m.regs { 46 | t.Errorf("regs wrong, want: %d, got: %d", regs, m.regs) 47 | } 48 | if unregs != m.unregs { 49 | t.Errorf("unregs wrong, want: %d, got: %d", unregs, m.unregs) 50 | } 51 | if cmds != m.cmds { 52 | t.Errorf("cmds wrong, want: %d, got: %d", cmds, m.cmds) 53 | } 54 | if uncmds != m.uncmds { 55 | t.Errorf("uncmds wrong, want: %d, got: %d", uncmds, m.uncmds) 56 | } 57 | } 58 | 59 | func TestHolder_New(t *testing.T) { 60 | t.Parallel() 61 | 62 | m := &mockReg{} 63 | h := newHolder(m) 64 | 65 | if h.registrar != m { 66 | t.Error("registrar is wrong") 67 | } 68 | if h.events == nil { 69 | t.Error("events is nil") 70 | } 71 | if h.commands == nil { 72 | t.Error("commands is nil") 73 | } 74 | 75 | m.verifyMock(t, 0, 0, 0, 0) 76 | } 77 | 78 | func TestHolder_Register(t *testing.T) { 79 | t.Parallel() 80 | 81 | m := &mockReg{} 82 | h := newHolder(m) 83 | 84 | id := h.Register("n", "c", "e", nil) 85 | if _, ok := h.events[id]; !ok { 86 | t.Error("did not record the registration") 87 | } 88 | 89 | m.verifyMock(t, 1, 0, 0, 0) 90 | } 91 | 92 | func TestHolder_Unregister(t *testing.T) { 93 | t.Parallel() 94 | 95 | m := &mockReg{ret: true} 96 | h := newHolder(m) 97 | 98 | id := h.Register("n", "c", "e", nil) 99 | if !h.Unregister(id) { 100 | t.Error("should be true") 101 | } 102 | if _, ok := h.events[id]; ok { 103 | t.Error("did not delete the registration") 104 | } 105 | 106 | m.verifyMock(t, 1, 1, 0, 0) 107 | } 108 | 109 | func TestHolder_UnregisterFail(t *testing.T) { 110 | t.Parallel() 111 | 112 | m := &mockReg{ret: false} 113 | h := newHolder(m) 114 | 115 | id := h.Register("n", "c", "e", nil) 116 | if h.Unregister(id) { 117 | t.Error("should be false") 118 | } 119 | 120 | m.verifyMock(t, 1, 1, 0, 0) 121 | } 122 | 123 | func TestHolder_RegisterCmd(t *testing.T) { 124 | t.Parallel() 125 | 126 | m := &mockReg{} 127 | h := newHolder(m) 128 | 129 | id, err := h.RegisterCmd("n", "c", &cmd.Command{Name: "cmd", Extension: "e"}) 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | if _, ok := h.commands[id]; !ok { 134 | t.Error("did not record the registration") 135 | } 136 | 137 | m.verifyMock(t, 0, 0, 1, 0) 138 | } 139 | 140 | func TestHolder_RegisterCmdFail(t *testing.T) { 141 | t.Parallel() 142 | 143 | e := errors.New("failure") 144 | m := &mockReg{err: e} 145 | h := newHolder(m) 146 | 147 | id, err := h.RegisterCmd("n", "c", &cmd.Command{Name: "cmd", Extension: "e"}) 148 | if err != e { 149 | t.Error("wrong error:", err, "want:", e) 150 | } 151 | if _, ok := h.commands[id]; ok { 152 | t.Error("should not record the registration") 153 | } 154 | 155 | m.verifyMock(t, 0, 0, 1, 0) 156 | } 157 | 158 | func TestHolder_UnregisterCmd(t *testing.T) { 159 | t.Parallel() 160 | 161 | m := &mockReg{} 162 | h := newHolder(m) 163 | 164 | id, err := h.RegisterCmd("n", "c", &cmd.Command{Name: "cmd", Extension: "e"}) 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | 169 | m.ret = true 170 | if ok := h.UnregisterCmd(id); !ok { 171 | t.Error("command not found") 172 | } 173 | 174 | if _, ok := h.commands[id]; ok { 175 | t.Error("did not delete the registration") 176 | } 177 | 178 | m.verifyMock(t, 0, 0, 1, 1) 179 | } 180 | 181 | func TestHolder_UnregisterCmdFail(t *testing.T) { 182 | t.Parallel() 183 | 184 | m := &mockReg{ret: false} 185 | h := newHolder(m) 186 | 187 | id, err := h.RegisterCmd("n", "c", &cmd.Command{Name: "cmd", Extension: "e"}) 188 | if err != nil { 189 | t.Error(err) 190 | } 191 | 192 | m.ret = false 193 | if ok := h.UnregisterCmd(id); ok { 194 | t.Error("command was found, should not have been found") 195 | } 196 | if _, ok := h.commands[id]; ok { 197 | t.Error("did not delete the registration") 198 | } 199 | 200 | m.verifyMock(t, 0, 0, 1, 1) 201 | } 202 | -------------------------------------------------------------------------------- /dispatch/trie.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | // errTrieNotUnique is a sentinel id value that occurs when an id was 9 | // not consumed and therefore nothing was inserted 10 | const errTrieNotUnique = 0 11 | 12 | var handlerList = sync.Pool{ 13 | New: func() interface{} { 14 | return make([]interface{}, 0) 15 | }, 16 | } 17 | 18 | func getHandlerList() []interface{} { 19 | list := handlerList.Get().([]interface{}) 20 | return list[:0] 21 | } 22 | 23 | func putHandlerList(list []interface{}) { 24 | handlerList.Put(list) 25 | } 26 | 27 | // trie is a prefix tree, not goroutine safe 28 | type trie struct { 29 | counter uint64 30 | isUnique bool 31 | root *trieNode 32 | } 33 | 34 | type trieNode struct { 35 | subtrees map[string]*trieNode 36 | handlers map[uint64]interface{} 37 | } 38 | 39 | func newTrie(isUnique bool) *trie { 40 | return &trie{ 41 | root: newTrieNode(), 42 | isUnique: isUnique, 43 | } 44 | } 45 | 46 | func newTrieNode() *trieNode { 47 | return &trieNode{ 48 | subtrees: make(map[string]*trieNode), 49 | } 50 | } 51 | 52 | func (t *trie) register(network, channel, event string, handler interface{}) uint64 { 53 | toInsert := []string{ 54 | strings.ToLower(network), 55 | strings.ToLower(channel), 56 | strings.ToLower(event), 57 | } 58 | return t.insert(t.root, toInsert, handler) 59 | } 60 | 61 | func (t *trie) insert(node *trieNode, toInsert []string, handler interface{}) uint64 { 62 | insert := toInsert[0] 63 | 64 | nextNode, ok := node.subtrees[insert] 65 | if !ok { 66 | nextNode = newTrieNode() 67 | node.subtrees[insert] = nextNode 68 | } 69 | 70 | if len(toInsert) == 1 { 71 | if t.isUnique && ok { 72 | return errTrieNotUnique 73 | } 74 | 75 | if nextNode.handlers == nil { 76 | nextNode.handlers = make(map[uint64]interface{}) 77 | } 78 | t.counter++ 79 | nextNode.handlers[t.counter] = handler 80 | return t.counter 81 | } 82 | 83 | toInsert = toInsert[1:] 84 | return t.insert(nextNode, toInsert, handler) 85 | } 86 | 87 | func (t *trie) handlers(network, channel, event string) []interface{} { 88 | toFind := []string{ 89 | strings.ToLower(network), 90 | strings.ToLower(channel), 91 | strings.ToLower(event), 92 | } 93 | list := getHandlerList() 94 | 95 | t.find(t.root, toFind, &list) 96 | 97 | retList := make([]interface{}, len(list)) 98 | copy(retList, list) 99 | putHandlerList(list) 100 | 101 | return retList 102 | } 103 | 104 | func (t *trie) find(node *trieNode, toFind []string, list *[]interface{}) { 105 | if len(toFind) == 0 { 106 | for _, h := range node.handlers { 107 | *list = append(*list, h) 108 | } 109 | return 110 | } 111 | 112 | find := toFind[0] 113 | 114 | if nextNode, ok := node.subtrees[""]; ok { 115 | t.find(nextNode, toFind[1:], list) 116 | } 117 | 118 | // This can happen if "channel" is nil, and in which case we don't want 119 | // to look ourselves up twice. 120 | if len(find) == 0 { 121 | return 122 | } 123 | if nextNode, ok := node.subtrees[find]; ok { 124 | t.find(nextNode, toFind[1:], list) 125 | } 126 | } 127 | 128 | func (t *trie) unregister(id uint64) bool { 129 | found, _ := t.unregisterHelper(t.root, id) 130 | return found 131 | } 132 | 133 | func (t *trie) unregisterHelper(node *trieNode, toFind uint64) (found, empty bool) { 134 | for id := range node.handlers { 135 | if id == toFind { 136 | delete(node.handlers, toFind) 137 | return true, len(node.handlers) == 0 138 | } 139 | } 140 | 141 | for k, n := range node.subtrees { 142 | f, e := t.unregisterHelper(n, toFind) 143 | if f { 144 | if e { 145 | delete(node.subtrees, k) 146 | } 147 | return true, len(node.subtrees) == 0 148 | } 149 | } 150 | 151 | return false, false 152 | } 153 | 154 | func (t *trie) allHandlers(network, channel string) []interface{} { 155 | list := getHandlerList() 156 | 157 | network = strings.ToLower(network) 158 | channel = strings.ToLower(channel) 159 | 160 | findAll(t.root, []string{network, channel, ""}, &list) 161 | 162 | retList := make([]interface{}, len(list)) 163 | copy(retList, list) 164 | putHandlerList(list) 165 | 166 | return list 167 | } 168 | 169 | func findAll(node *trieNode, toFind []string, list *[]interface{}) { 170 | for _, h := range node.handlers { 171 | *list = append(*list, h) 172 | } 173 | if len(toFind) == 0 { 174 | return 175 | } 176 | 177 | find := toFind[0] 178 | 179 | // This can happen if "channel" is nil, and in which case we don't want 180 | // to look ourselves up twice. 181 | if len(find) == 0 { 182 | for _, subtree := range node.subtrees { 183 | findAll(subtree, toFind[1:], list) 184 | } 185 | return 186 | } 187 | 188 | if nextNode, ok := node.subtrees[find]; ok { 189 | findAll(nextNode, toFind[1:], list) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /plan.txt: -------------------------------------------------------------------------------- 1 | // 2 | // All requests must have a header "Auth" which carries an empty JWT token 3 | // 4 | 5 | PUT /api/v1/register 6 | { 7 | name: "myext", 8 | handlers: [ 9 | { 10 | network: "something", // Optional 11 | channel: "something", // Optional 12 | event: "001", 13 | }, 14 | ], 15 | commands: [ 16 | { 17 | network: "something", // Optional 18 | channel: "something", // Optional 19 | cmd: { 20 | name: "cmd", 21 | extension: "name", 22 | description: "description", 23 | kind: 1, 24 | scope: 1, 25 | args: ["arg", "two"], 26 | require_auth: true, 27 | req_level: 5, 28 | req_flags: "ab" 29 | } 30 | } 31 | ], 32 | } 33 | 34 | Response: 35 | 200 OK 36 | 37 | ///////////////////////////////////////// 38 | 39 | PUT /api/v1/unregister 40 | { 41 | name: "myext" 42 | } 43 | 44 | Response: 45 | 200 OK 46 | 47 | ///////////////////////////////////////// 48 | State: 49 | ///////////////////////////////////////// 50 | 51 | GET /api/v1/net/{network}/state/self 52 | { 53 | "user": { "host": "who!who@who.com", "name": "Who Bot" }, 54 | "modes": CHANNELMODES 55 | } 56 | 57 | GET /api/v1/net/{network}/state/user/{user} 58 | Response: 200 404 { "host": "fish!fish@fish.com", "name": "Dylan Johnstoner" } 59 | GET /api/v1/net/{network}/state/users?channel="#channel" 60 | Response: 200 404 [ "fish!fish@fish.com", "cm!cm@cm.com" ] 61 | GET /api/v1/net/{network}/state/users/count?channel="#channel" 62 | Response: 200 404 { "count": N } 63 | 64 | GET /api/v1/net/{network}/state/user_modes/{channel}/{nick_or_host} 65 | Response: 200 404 66 | CHANNELMODES 67 | 68 | GET /api/v1/net/{network}/state/channel/{channel} 69 | Response: 200 404 70 | { 71 | "name": "#deviate", 72 | "topic": "meetup at cm's house whatever... just show up and say hello!" 73 | "modes": { 74 | "arg_modes": { 75 | "l": "5", 76 | "k": "password" 77 | }, 78 | "address_modes": { 79 | "b": ["fish*", "cm*"], 80 | }, 81 | } 82 | } 83 | GET /api/v1/net/{network}/state/channels?user="fish" 84 | Response: 200 ["#deviate", "#bots"] 85 | GET /api/v1/net/{network}/state/channels/count?user="fish" 86 | Response: 200 404 { "count": N } 87 | 88 | GET /api/v1/net/{network}/state/is_on/{channel}/{nick_or_host} 89 | Response: 200 404 90 | 91 | CHANNELMODES: 92 | { 93 | "modes": N, 94 | "kinds": { 95 | "user_prefixs": [ 96 | { "symbol": "@", "char": "o" }, 97 | ] 98 | "channel_modes": { 99 | "l": 2, 100 | "b": 4, 101 | } 102 | } 103 | } 104 | 105 | ///////////////////////////////////////// 106 | Store: 107 | ///////////////////////////////////////// 108 | 109 | PUT /api/v1/auth_user 110 | { 111 | "network": "irc.zkpq.ca", 112 | "host": "fish!fish@fish.com", 113 | "username": "username", 114 | "password": "bcrypt", 115 | "permanent": true 116 | } 117 | Response: 200 / 401 118 | 119 | GET /api/v1/store/net/{network}/authed_user/{host} 120 | Response: 200 / 404 121 | STOREDUSER 122 | 123 | GET /api/v1/store/user/{username} 124 | Response: 200 / 404 125 | STOREDUSER 126 | 127 | GET /api/v1/store/users 128 | Response: 200 129 | [ STOREDUSER ] 130 | 131 | GET /api/v1/store/net/{network}/users 132 | Response: 200 / 404 133 | [ STOREDUSER ] 134 | 135 | GET /api/v1/store/net/{network}/channel/{channel}/users 136 | Response: 200 / 404 137 | [ STOREDUSER ] 138 | 139 | GET /api/v1/store/net/{network}/channel/{channel} 140 | Response: 200 / 404 141 | STOREDCHANNEL 142 | 143 | GET /api/v1/store/channels 144 | Response: 200 145 | [ STOREDCHANNEL ] 146 | 147 | PUT /api/v1/store/user 148 | STOREDUSER 149 | Response: 200 500 150 | { "error": "description" } 151 | 152 | PUT /api/v1/store/channel 153 | STOREDCHANNEL 154 | Response: 200 500 155 | { "error": "description" } 156 | 157 | DELETE /api/v1/store/user/{username} 158 | Response: 200 404 159 | 160 | DELETE /api/v1/store/net/{network}/channel/{channel} 161 | Response: 200 404 162 | 163 | // Username OR Net+Host 164 | DELETE /api/v1/logout?network={network}&host={host}&username={username} 165 | Response: 200 404 166 | 167 | STOREDUSER: 168 | { 169 | "username": "stuff", 170 | "password": "bcrypt", 171 | "masks": ["*!*@fish.com"], 172 | "access": { 173 | "irc.zkpq.ca:#deviate": ACCESS 174 | } 175 | "data": { 176 | Arbitrary Key Value Data store string -> string 177 | } 178 | } 179 | 180 | STOREDCHANNEL: 181 | { 182 | "network": "irc.zkpq.ca", 183 | "name": "#deviate", 184 | "data": { 185 | Arbitrary Key Value Data store string -> string 186 | } 187 | } 188 | 189 | ACCESS: 190 | { 191 | "level": 10, 192 | "flags": "weEdpants", 193 | } 194 | -------------------------------------------------------------------------------- /bot/pipe.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/aarondl/ultimateq/api" 7 | 8 | "github.com/aarondl/ultimateq/dispatch" 9 | "github.com/aarondl/ultimateq/dispatch/cmd" 10 | "github.com/aarondl/ultimateq/irc" 11 | "gopkg.in/inconshreveable/log15.v2" 12 | ) 13 | 14 | const ( 15 | // The initial error in the subscriber on Send() counts for a misfire of 16 | // sorts 17 | misfireThreshold = 4 18 | ) 19 | 20 | var _ dispatch.Handler = &pipeHandler{} 21 | var _ cmd.Handler = &pipeHandler{} 22 | 23 | type pipeHelper interface { 24 | broadcastEvent(ext string, r *api.IRCEventResponse) bool 25 | broadcastCmd(ext string, r *api.CmdEventResponse) bool 26 | unregEvent(ext string, id uint64) 27 | unregCmd(ext string, id uint64) 28 | } 29 | 30 | type pipeHandler struct { 31 | logger log15.Logger 32 | ext string 33 | 34 | helper pipeHelper 35 | 36 | // A pipeHandler briefly exists during a time where there could be no 37 | // eventID set (after register) but events are firing (before eventID can 38 | // be set) so this protects eventID while setting initially. 39 | // 40 | // misfires must also be protected since event handlers are fired from 41 | // multiple goroutines and they're technically editing the data 42 | mut sync.RWMutex 43 | eventID uint64 44 | // Misfires is incremented every time this handler fails to deliver 45 | // to at least one remote subscriber. It marks obsolesence and will be 46 | // garbage collected upon reaching a threshold. 47 | misfires int 48 | } 49 | 50 | func (p *pipeHandler) setEventID(evID uint64) { 51 | p.mut.Lock() 52 | p.eventID = evID 53 | p.mut.Unlock() 54 | } 55 | 56 | func (p *pipeHandler) Handle(w irc.Writer, ev *irc.Event) { 57 | p.mut.RLock() 58 | evID := p.eventID 59 | p.mut.RUnlock() 60 | if evID == 0 { 61 | return 62 | } 63 | 64 | p.logger.Debug("remote event dispatch", "id", evID) 65 | 66 | event := &api.IRCEventResponse{ 67 | Id: evID, 68 | Event: &api.IRCEvent{ 69 | Name: ev.Name, 70 | Sender: ev.Sender, 71 | Args: ev.Args, 72 | Time: ev.Time.Unix(), 73 | Net: ev.NetworkID, 74 | }, 75 | } 76 | 77 | sent := p.helper.broadcastEvent(p.ext, event) 78 | if sent { 79 | return 80 | } 81 | 82 | p.logger.Debug("remote misfire", "ext", p.ext, "id", evID) 83 | 84 | var misfires int 85 | p.mut.Lock() 86 | p.misfires++ 87 | misfires = p.misfires 88 | p.mut.Unlock() 89 | 90 | if misfires > misfireThreshold { 91 | p.logger.Debug("unreg event misfire threshold", "ext", p.ext, "id", evID) 92 | p.helper.unregEvent(p.ext, evID) 93 | } 94 | } 95 | 96 | func (p *pipeHandler) Cmd(name string, w irc.Writer, ev *cmd.Event) error { 97 | p.mut.RLock() 98 | evID := p.eventID 99 | p.mut.RUnlock() 100 | if evID == 0 { 101 | return nil 102 | } 103 | 104 | p.logger.Debug("remote cmd dispatch", "id", evID) 105 | 106 | iev := &api.IRCEvent{ 107 | Name: ev.Event.Name, 108 | Sender: ev.Event.Sender, 109 | Args: ev.Event.Args, 110 | Time: ev.Event.Time.Unix(), 111 | Net: ev.Event.NetworkID, 112 | } 113 | 114 | command := &api.CmdEventResponse{ 115 | Id: evID, 116 | Name: name, 117 | Event: &api.CmdEvent{ 118 | IrcEvent: iev, 119 | Args: ev.Args, 120 | }, 121 | } 122 | 123 | if ev.User != nil { 124 | command.Event.User = ev.User.ToProto() 125 | } 126 | if ev.StoredUser != nil { 127 | command.Event.StoredUser = ev.StoredUser.ToProto() 128 | } 129 | if ev.UserChannelModes != nil { 130 | command.Event.UserChanModes = ev.UserChannelModes.ToProto() 131 | } 132 | if ev.Channel != nil { 133 | command.Event.Channel = ev.Channel.ToProto() 134 | } 135 | if ev.TargetChannel != nil { 136 | command.Event.TargetChannel = ev.TargetChannel.ToProto() 137 | } 138 | if ev.TargetUsers != nil { 139 | targs := make(map[string]*api.StateUser, len(ev.TargetUsers)) 140 | for k, v := range ev.TargetUsers { 141 | targs[k] = v.ToProto() 142 | } 143 | command.Event.TargetUsers = targs 144 | } 145 | if ev.TargetStoredUsers != nil { 146 | targs := make(map[string]*api.StoredUser, len(ev.TargetStoredUsers)) 147 | for k, v := range ev.TargetStoredUsers { 148 | targs[k] = v.ToProto() 149 | } 150 | command.Event.TargetStoredUsers = targs 151 | } 152 | if ev.TargetVarUsers != nil { 153 | targs := make([]*api.StateUser, len(ev.TargetVarUsers)) 154 | for i, v := range ev.TargetVarUsers { 155 | targs[i] = v.ToProto() 156 | } 157 | command.Event.TargetVariadicUsers = targs 158 | } 159 | if ev.TargetVarStoredUsers != nil { 160 | targs := make([]*api.StoredUser, len(ev.TargetVarStoredUsers)) 161 | for i, v := range ev.TargetVarStoredUsers { 162 | targs[i] = v.ToProto() 163 | } 164 | command.Event.TargetVariadicStoredUsers = targs 165 | } 166 | 167 | sent := p.helper.broadcastCmd(p.ext, command) 168 | if sent { 169 | return nil 170 | } 171 | 172 | p.logger.Debug("remote misfire", "ext", p.ext, "id", evID) 173 | 174 | var misfires int 175 | p.mut.Lock() 176 | p.misfires++ 177 | misfires = p.misfires 178 | p.mut.Unlock() 179 | 180 | if misfires > misfireThreshold { 181 | p.logger.Debug("unreg cmd misfire threshold", "ext", p.ext, "id", evID) 182 | p.helper.unregEvent(p.ext, evID) 183 | } 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /data/channel_modes_diff_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func TestModeDiff_Create(t *testing.T) { 9 | t.Parallel() 10 | m := NewModeDiff(testKinds) 11 | var _ moder = &m 12 | } 13 | 14 | func TestModeDiff_Clone(t *testing.T) { 15 | t.Parallel() 16 | 17 | diff := NewModeDiff(testKinds) 18 | diff.Apply("a-m") 19 | clone := diff.Clone() 20 | if !clone.IsSet("a") || !clone.IsUnset("m") { 21 | t.Error("Expected a to be set and m to be unset.") 22 | } 23 | } 24 | 25 | func TestModeDiff_Apply(t *testing.T) { 26 | t.Parallel() 27 | 28 | d := NewModeDiff(testKinds) 29 | pos, neg := d.Apply("+ab-c 10 ") 30 | if got, exp := len(pos), 0; exp != got { 31 | t.Errorf("Expected: %v, got: %v", exp, got) 32 | } 33 | if got, exp := len(neg), 0; exp != got { 34 | t.Errorf("Expected: %v, got: %v", exp, got) 35 | } 36 | if got, exp := d.IsSet("ab 10"), true; exp != got { 37 | t.Errorf("Expected: %v, got: %v", exp, got) 38 | } 39 | if got, exp := d.IsSet("c"), false; exp != got { 40 | t.Errorf("Expected: %v, got: %v", exp, got) 41 | } 42 | if got, exp := d.IsUnset("c"), false; exp != got { 43 | t.Errorf("Expected: %v, got: %v", exp, got) 44 | } 45 | 46 | d = NewModeDiff(testKinds) 47 | pos, neg = d.Apply("+b-b 10 10") 48 | if got, exp := len(pos), 0; exp != got { 49 | t.Errorf("Expected: %v, got: %v", exp, got) 50 | } 51 | if got, exp := len(neg), 0; exp != got { 52 | t.Errorf("Expected: %v, got: %v", exp, got) 53 | } 54 | if got, exp := d.IsSet("b 10"), false; exp != got { 55 | t.Errorf("Expected: %v, got: %v", exp, got) 56 | } 57 | if got, exp := d.IsUnset("b 10"), true; exp != got { 58 | t.Errorf("Expected: %v, got: %v", exp, got) 59 | } 60 | 61 | d = NewModeDiff(testKinds) 62 | pos, neg = d.Apply("-b+b 10 10") 63 | if got, exp := len(pos), 0; exp != got { 64 | t.Errorf("Expected: %v, got: %v", exp, got) 65 | } 66 | if got, exp := len(neg), 0; exp != got { 67 | t.Errorf("Expected: %v, got: %v", exp, got) 68 | } 69 | if got, exp := d.IsSet("b 10"), true; exp != got { 70 | t.Errorf("Expected: %v, got: %v", exp, got) 71 | } 72 | if got, exp := d.IsUnset("b 10"), false; exp != got { 73 | t.Errorf("Expected: %v, got: %v", exp, got) 74 | } 75 | 76 | pos, neg = d.Apply("+x-y+z") 77 | if got, exp := len(pos), 0; exp != got { 78 | t.Errorf("Expected: %v, got: %v", exp, got) 79 | } 80 | if got, exp := len(neg), 0; exp != got { 81 | t.Errorf("Expected: %v, got: %v", exp, got) 82 | } 83 | if got, exp := d.IsSet("x"), true; exp != got { 84 | t.Errorf("Expected: %v, got: %v", exp, got) 85 | } 86 | if got, exp := d.IsUnset("y"), true; exp != got { 87 | t.Errorf("Expected: %v, got: %v", exp, got) 88 | } 89 | if got, exp := d.IsSet("z"), true; exp != got { 90 | t.Errorf("Expected: %v, got: %v", exp, got) 91 | } 92 | if got, exp := d.IsUnset("x"), false; exp != got { 93 | t.Errorf("Expected: %v, got: %v", exp, got) 94 | } 95 | if got, exp := d.IsSet("y"), false; exp != got { 96 | t.Errorf("Expected: %v, got: %v", exp, got) 97 | } 98 | if got, exp := d.IsUnset("z"), false; exp != got { 99 | t.Errorf("Expected: %v, got: %v", exp, got) 100 | } 101 | 102 | pos, neg = d.Apply("+vx-yo+vz user1 user2 user3") 103 | if got, exp := len(pos), 2; exp != got { 104 | t.Errorf("Expected: %v, got: %v", exp, got) 105 | } 106 | if got, exp := len(neg), 1; exp != got { 107 | t.Errorf("Expected: %v, got: %v", exp, got) 108 | } 109 | if got, exp := pos[0].Mode, 'v'; exp != got { 110 | t.Errorf("Expected: %v, got: %v", exp, got) 111 | } 112 | if got, exp := pos[0].Arg, "user1"; exp != got { 113 | t.Errorf("Expected: %v, got: %v", exp, got) 114 | } 115 | if got, exp := pos[1].Mode, 'v'; exp != got { 116 | t.Errorf("Expected: %v, got: %v", exp, got) 117 | } 118 | if got, exp := pos[1].Arg, "user3"; exp != got { 119 | t.Errorf("Expected: %v, got: %v", exp, got) 120 | } 121 | if got, exp := neg[0].Mode, 'o'; exp != got { 122 | t.Errorf("Expected: %v, got: %v", exp, got) 123 | } 124 | if got, exp := neg[0].Arg, "user2"; exp != got { 125 | t.Errorf("Expected: %v, got: %v", exp, got) 126 | } 127 | if got, exp := d.IsSet("x"), true; exp != got { 128 | t.Errorf("Expected: %v, got: %v", exp, got) 129 | } 130 | if got, exp := d.IsUnset("y"), true; exp != got { 131 | t.Errorf("Expected: %v, got: %v", exp, got) 132 | } 133 | if got, exp := d.IsSet("z"), true; exp != got { 134 | t.Errorf("Expected: %v, got: %v", exp, got) 135 | } 136 | if got, exp := d.IsUnset("x"), false; exp != got { 137 | t.Errorf("Expected: %v, got: %v", exp, got) 138 | } 139 | if got, exp := d.IsSet("y"), false; exp != got { 140 | t.Errorf("Expected: %v, got: %v", exp, got) 141 | } 142 | if got, exp := d.IsUnset("z"), false; exp != got { 143 | t.Errorf("Expected: %v, got: %v", exp, got) 144 | } 145 | } 146 | 147 | func TestModeDiff_String(t *testing.T) { 148 | t.Parallel() 149 | 150 | diff := NewModeDiff(testKinds) 151 | diff.pos.Set("a", "b host1", "c 1") 152 | diff.neg.Set("x", "y", "z", "b host2") 153 | str := diff.String() 154 | matched, err := regexp.MatchString( 155 | `^\+[abc]{3}-[xyzb]{4}( 1| host1){2}( host2){1}$`, str) 156 | if err != nil { 157 | t.Error("Regexp failed to compile:", err) 158 | } 159 | if !matched { 160 | t.Errorf("Expected: %q to match the pattern.", str) 161 | } 162 | 163 | diff = NewModeDiff(testKinds) 164 | diff.pos.Set("x", "y", "z") 165 | diff.neg.Set("x", "y", "z") 166 | str = diff.String() 167 | matched, err = regexp.MatchString(`^\+[xyz]{3}-[xyz]{3}$`, str) 168 | if err != nil { 169 | t.Error("Regexp failed to compile:", err) 170 | } 171 | if !matched { 172 | t.Errorf("Expected: %q to match the pattern.", str) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /irc/proto.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | // IRC Events, these events are 1-1 constant to string lookups for ease of 4 | // use when registering handlers etc. 5 | const ( 6 | JOIN = "JOIN" 7 | KICK = "KICK" 8 | MODE = "MODE" 9 | NICK = "NICK" 10 | NOTICE = "NOTICE" 11 | PART = "PART" 12 | PING = "PING" 13 | PONG = "PONG" 14 | PRIVMSG = "PRIVMSG" 15 | QUIT = "QUIT" 16 | TOPIC = "TOPIC" 17 | 18 | CTCP = PRIVMSG 19 | CTCPReply = NOTICE 20 | ) 21 | 22 | // IRC Reply and Error Events. These are sent in reply to a previous event. 23 | const ( 24 | RPL_WELCOME = "001" 25 | RPL_YOURHOST = "002" 26 | RPL_CREATED = "003" 27 | RPL_MYINFO = "004" 28 | RPL_ISUPPORT = "005" 29 | RPL_BOUNCE = "005" 30 | RPL_USERHOST = "302" 31 | RPL_ISON = "303" 32 | RPL_AWAY = "301" 33 | RPL_UNAWAY = "305" 34 | RPL_NOWAWAY = "306" 35 | RPL_WHOISUSER = "311" 36 | RPL_WHOISSERVER = "312" 37 | RPL_WHOISOPERATOR = "313" 38 | RPL_WHOISIDLE = "317" 39 | RPL_ENDOFWHOIS = "318" 40 | RPL_WHOISCHANNELS = "319" 41 | RPL_WHOWASUSER = "314" 42 | RPL_ENDOFWHOWAS = "369" 43 | RPL_LISTSTART = "321" 44 | RPL_LIST = "322" 45 | RPL_LISTEND = "323" 46 | RPL_UNIQOPIS = "325" 47 | RPL_CHANNELMODEIS = "324" 48 | RPL_NOTOPIC = "331" 49 | RPL_TOPIC = "332" 50 | RPL_INVITING = "341" 51 | RPL_SUMMONING = "342" 52 | RPL_INVITELIST = "346" 53 | RPL_ENDOFINVITELIST = "347" 54 | RPL_EXCEPTLIST = "348" 55 | RPL_ENDOFEXCEPTLIST = "349" 56 | RPL_VERSION = "351" 57 | RPL_WHOREPLY = "352" 58 | RPL_ENDOFWHO = "315" 59 | RPL_NAMREPLY = "353" 60 | RPL_ENDOFNAMES = "366" 61 | RPL_LINKS = "364" 62 | RPL_ENDOFLINKS = "365" 63 | RPL_BANLIST = "367" 64 | RPL_ENDOFBANLIST = "368" 65 | RPL_INFO = "371" 66 | RPL_ENDOFINFO = "374" 67 | RPL_MOTDSTART = "375" 68 | RPL_MOTD = "372" 69 | RPL_ENDOFMOTD = "376" 70 | RPL_YOUREOPER = "381" 71 | RPL_REHASHING = "382" 72 | RPL_YOURESERVICE = "383" 73 | RPL_TIME = "391" 74 | RPL_USERSSTART = "392" 75 | RPL_USERS = "393" 76 | RPL_ENDOFUSERS = "394" 77 | RPL_NOUSERS = "395" 78 | RPL_TRACELINK = "200" 79 | RPL_TRACECONNECTING = "201" 80 | RPL_TRACEHANDSHAKE = "202" 81 | RPL_TRACEUNKNOWN = "203" 82 | RPL_TRACEOPERATOR = "204" 83 | RPL_TRACEUSER = "205" 84 | RPL_TRACESERVER = "206" 85 | RPL_TRACESERVICE = "207" 86 | RPL_TRACENEWTYPE = "208" 87 | RPL_TRACECLASS = "209" 88 | RPL_TRACERECONNECT = "210" 89 | RPL_TRACELOG = "261" 90 | RPL_TRACEEND = "262" 91 | RPL_STATSLINKINFO = "211" 92 | RPL_STATSCOMMANDS = "212" 93 | RPL_ENDOFSTATS = "219" 94 | RPL_STATSUPTIME = "242" 95 | RPL_STATSOLINE = "243" 96 | RPL_UMODEIS = "221" 97 | RPL_SERVLIST = "234" 98 | RPL_SERVLISTEND = "235" 99 | RPL_LUSERCLIENT = "251" 100 | RPL_LUSEROP = "252" 101 | RPL_LUSERUNKNOWN = "253" 102 | RPL_LUSERCHANNELS = "254" 103 | RPL_LUSERME = "255" 104 | RPL_ADMINME = "256" 105 | RPL_ADMINLOC1 = "257" 106 | RPL_ADMINLOC2 = "258" 107 | RPL_ADMINEMAIL = "259" 108 | RPL_TRYAGAIN = "263" 109 | 110 | ERR_NOSUCHNICK = "401" 111 | ERR_NOSUCHSERVER = "402" 112 | ERR_NOSUCHCHANNEL = "403" 113 | ERR_CANNOTSENDTOCHAN = "404" 114 | ERR_TOOMANYCHANNELS = "405" 115 | ERR_WASNOSUCHNICK = "406" 116 | ERR_TOOMANYTARGETS = "407" 117 | ERR_NOSUCHSERVICE = "408" 118 | ERR_NOORIGIN = "409" 119 | ERR_NORECIPIENT = "411" 120 | ERR_NOTEXTTOSEND = "412" 121 | ERR_NOTOPLEVEL = "413" 122 | ERR_WILDTOPLEVEL = "414" 123 | ERR_BADMASK = "415" 124 | ERR_UNKNOWNCOMMAND = "421" 125 | ERR_NOMOTD = "422" 126 | ERR_NOADMININFO = "423" 127 | ERR_FILEERROR = "424" 128 | ERR_NONICKNAMEGIVEN = "431" 129 | ERR_ERRONEUSNICKNAME = "432" 130 | ERR_NICKNAMEINUSE = "433" 131 | ERR_NICKCOLLISION = "436" 132 | ERR_UNAVAILRESOURCE = "437" 133 | ERR_USERNOTINCHANNEL = "441" 134 | ERR_NOTONCHANNEL = "442" 135 | ERR_USERONCHANNEL = "443" 136 | ERR_NOLOGIN = "444" 137 | ERR_SUMMONDISABLED = "445" 138 | ERR_USERSDISABLED = "446" 139 | ERR_NOTREGISTERED = "451" 140 | ERR_NEEDMOREPARAMS = "461" 141 | ERR_ALREADYREGISTRED = "462" 142 | ERR_NOPERMFORHOST = "463" 143 | ERR_PASSWDMISMATCH = "464" 144 | ERR_YOUREBANNEDCREEP = "465" 145 | ERR_YOUWILLBEBANNED = "466" 146 | ERR_KEYSET = "467" 147 | ERR_CHANNELISFULL = "471" 148 | ERR_UNKNOWNMODE = "472" 149 | ERR_INVITEONLYCHAN = "473" 150 | ERR_BANNEDFROMCHAN = "474" 151 | ERR_BADCHANNELKEY = "475" 152 | ERR_BADCHANMASK = "476" 153 | ERR_NOCHANMODES = "477" 154 | ERR_BANLISTFULL = "478" 155 | ERR_NOPRIVILEGES = "481" 156 | ERR_CHANOPRIVSNEEDED = "482" 157 | ERR_CANTKILLSERVER = "483" 158 | ERR_RESTRICTED = "484" 159 | ERR_UNIQOPPRIVSNEEDED = "485" 160 | ERR_NOOPERHOST = "491" 161 | ERR_UMODEUNKNOWNFLAG = "501" 162 | ERR_USERSDONTMATCH = "502" 163 | ) 164 | 165 | // Pseudo Events, these events are not real events defined by the irc 166 | // protocol but the bot provides them to allow for additional events to be 167 | // handled such as connect or disconnects which the irc protocol has no protocol 168 | // defined for. 169 | const ( 170 | RAW = "RAW" 171 | CONNECT = "CONNECT" 172 | DISCONNECT = "DISCONNECT" 173 | ) 174 | -------------------------------------------------------------------------------- /bot/botconfig_test.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "testing" 7 | 8 | "github.com/aarondl/ultimateq/config" 9 | "github.com/aarondl/ultimateq/irc" 10 | "github.com/aarondl/ultimateq/mocks" 11 | "gopkg.in/inconshreveable/log15.v2" 12 | ) 13 | 14 | var zeroConnProvider = func(srv string) (net.Conn, error) { 15 | return nil, nil 16 | } 17 | 18 | func TestBotConfig_ReadConfig(t *testing.T) { 19 | b, _ := createBot(fakeConfig, nil, nil, devNull, false, false) 20 | 21 | b.ReadConfig(func(conf *config.Config) { 22 | got, _ := conf.Network(netID).Nick() 23 | exp, _ := conf.Network(netID).Nick() 24 | if got != exp { 25 | t.Error("The names should have been the same.") 26 | } 27 | }) 28 | } 29 | 30 | func TestBotConfig_WriteConfig(t *testing.T) { 31 | b, _ := createBot(fakeConfig, nil, nil, devNull, false, false) 32 | 33 | b.WriteConfig(func(conf *config.Config) { 34 | got, _ := conf.Network(netID).Nick() 35 | exp, _ := conf.Network(netID).Nick() 36 | if got != exp { 37 | t.Error("The names should have been the same.") 38 | } 39 | }) 40 | } 41 | 42 | func TestBotConfig_testElementEquals(t *testing.T) { 43 | a := []string{"a", "b"} 44 | b := []string{"b", "a"} 45 | if !contains(a, b) { 46 | t.Error("Expected equals.") 47 | } 48 | 49 | a = []string{"a", "b", "c"} 50 | if contains(a, b) { 51 | t.Error("Expected not equals.") 52 | } 53 | 54 | a = []string{"x", "y"} 55 | if contains(a, b) { 56 | t.Error("Expected not equals.") 57 | } 58 | 59 | a = []string{} 60 | b = []string{} 61 | if !contains(a, b) { 62 | t.Error("Expected equals.") 63 | } 64 | 65 | b = []string{"a"} 66 | if contains(a, b) { 67 | t.Error("Expected not equals.") 68 | } 69 | 70 | a = []string{"a"} 71 | b = []string{} 72 | if contains(a, b) { 73 | t.Error("Expected not equals.") 74 | } 75 | } 76 | 77 | func TestBotConfig_ReplaceConfig(t *testing.T) { 78 | nick := []byte(irc.NICK + " :newnick\r\n") 79 | 80 | conns := map[string]*mocks.Conn{ 81 | "irc.test.net": mocks.NewConn(), 82 | "newserver:6667": mocks.NewConn(), 83 | "anothernewserver:6667": mocks.NewConn(), 84 | } 85 | connProvider := func(srv string) (net.Conn, error) { 86 | c := conns[srv] 87 | if c == nil { 88 | panic("No connection found:" + srv) 89 | } 90 | return conns[srv], nil 91 | } 92 | 93 | /*chans1 := []string{"#chan1", "#chan2", "#chan3"} 94 | chans2 := []string{"#chan1", "#chan3"} 95 | chans3 := []string{"#chan1"}*/ 96 | 97 | c1 := fakeConfig.Clone() 98 | c1.NewNetwork("newserver").SetServers([]string{"newserver:6667"}) 99 | /*GlobalContext(). 100 | Channels(chans1...).*/ 101 | 102 | c2 := fakeConfig.Clone() 103 | //Channels(chans2...). 104 | c2.Network(netID).SetNick("newnick") 105 | c2.NewNetwork("anothernewserver"). 106 | SetServers([]string{"anothernewserver:6667"}) 107 | 108 | c3 := config.New() 109 | 110 | b, _ := createBot(c1, connProvider, nil, devNull, false, false) 111 | b.Logger.SetHandler(log15.DiscardHandler()) 112 | srvs := c1.Networks() 113 | if len(srvs) != len(b.servers) { 114 | t.Errorf("The number of servers (%v) should match the config (%v)", 115 | len(b.servers), len(srvs)) 116 | } 117 | 118 | oldsrv1, oldsrv2 := b.servers[netID], b.servers["newserver"] 119 | old1listen, old2listen := make(chan Status), make(chan Status) 120 | 121 | oldsrv1.addStatusListener(old1listen, STATUS_STARTED) 122 | oldsrv2.addStatusListener(old2listen, STATUS_STARTED) 123 | 124 | end := b.Start() 125 | 126 | <-old1listen 127 | <-old2listen 128 | 129 | /*if e := b.conf.Global.Channels; !contains(e, chans1) { 130 | t.Errorf("Expected elements: %v", e) 131 | } 132 | if e := oldsrv1.conf.GetChannels(); !contains(e, chans1) { 133 | t.Errorf("Expected elements: %v", e) 134 | } 135 | if e := oldsrv2.conf.GetChannels(); !contains(e, chans1) { 136 | t.Errorf("Expected elements: %v", e) 137 | } 138 | if e := b.dispatchCore.Channels(); !contains(e, chans1) { 139 | t.Errorf("Expected elements: %v", e) 140 | } 141 | if e := oldsrv1.dispatchCore.Channels(); !contains(e, chans1) { 142 | t.Errorf("Expected elements: %v", e) 143 | } 144 | if e := oldsrv2.dispatchCore.Channels(); !contains(e, chans1) { 145 | t.Errorf("Expected elements: %v", e) 146 | }*/ 147 | 148 | success := b.ReplaceConfig(c3) // Invalid Config 149 | if success { 150 | t.Error("An invalid config should fail.") 151 | } 152 | success = b.ReplaceConfig(c2) 153 | if !success { 154 | t.Error("A valid new config should succeed.") 155 | } 156 | 157 | if err := <-end; err != errServerKilled { 158 | t.Error("Expected a kill error:", err) 159 | } 160 | 161 | //newsrv1 := b.servers["anothernewserver"] 162 | 163 | srvs = c2.Networks() 164 | if len(srvs) != len(b.servers) { 165 | t.Errorf("The number of servers (%v) should match the config (%v)", 166 | len(b.servers), len(srvs)) 167 | } 168 | 169 | /*if e := b.conf.Global.Channels; !contains(e, chans2) { 170 | t.Errorf("Expected elements: %v", e) 171 | } 172 | if e := oldsrv1.conf.GetChannels(); !contains(e, chans3) { 173 | t.Errorf("Expected elements: %v", e) 174 | } 175 | if e := newsrv1.conf.GetChannels(); !contains(e, chans2) { 176 | t.Errorf("Expected elements: %v", e) 177 | } 178 | if e := b.dispatchCore.Channels(); !contains(e, chans2) { 179 | t.Errorf("Expected elements: %v", e) 180 | } 181 | if e := oldsrv1.dispatchCore.Channels(); !contains(e, chans3) { 182 | t.Errorf("Expected elements: %v", e) 183 | } 184 | if e := newsrv1.dispatchCore.Channels(); !contains(e, chans2) { 185 | t.Errorf("Expected elements: %v", e) 186 | }*/ 187 | 188 | recv := conns["irc.test.net"].Receive(len(nick), nil) 189 | if bytes.Compare(recv, nick) != 0 { 190 | t.Errorf("Was expecting a change in nick but got: %s", recv) 191 | } 192 | 193 | b.Stop() 194 | for range end { 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /irc/ctcp_test.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestIsCTCPHelper(t *testing.T) { 9 | ev := NewEvent("", nil, PRIVMSG, "", "user", "\x01DCC SEND\x01") 10 | if !ev.IsCTCP() { 11 | t.Error("Expected it to be a CTCP Event.") 12 | } 13 | 14 | ev = NewEvent("", nil, NOTICE, "", "user", "\x01DCC SEND\x01") 15 | if !ev.IsCTCP() { 16 | t.Error("Expected it to be a CTCP Event.") 17 | } 18 | 19 | ev = NewEvent("", nil, PRIVMSG, "", "user", "DCC SEND") 20 | if ev.IsCTCP() { 21 | t.Error("CTCP cannot be missing delimiter bytes.") 22 | } 23 | 24 | ev = NewEvent("", nil, JOIN, "", "user", "\x01DCC SEND\x01") 25 | if ev.IsCTCP() { 26 | t.Error("Only PRIVMSG and NOTICE can be CTCP events.") 27 | } 28 | } 29 | 30 | func TestUnpackCTCPHelper(t *testing.T) { 31 | ev := NewEvent("", nil, PRIVMSG, "", "user", "\x01DCC SEND\x01") 32 | tag, data := ev.UnpackCTCP() 33 | expectTag, expectData := CTCPunpackString(ev.Message()) 34 | 35 | if tag != expectTag { 36 | t.Error("Expected the tag to be the same as the helper it calls.") 37 | } 38 | if data != expectData { 39 | t.Error("Expected the data to be the same as the helper it calls.") 40 | } 41 | } 42 | 43 | func TestIsCTCP(t *testing.T) { 44 | yes, no := []byte("\x01yes\x01"), []byte("no") 45 | if !IsCTCP(yes) { 46 | t.Errorf("Expected (% X) to be a CTCP.", yes) 47 | } 48 | if IsCTCP(no) { 49 | t.Errorf("Expected (% X) to NOT be a CTCP.", no) 50 | } 51 | } 52 | 53 | func TestIsCTCPString(t *testing.T) { 54 | yes, no := "\x01yes\x01", "no" 55 | if !IsCTCPString(yes) { 56 | t.Errorf("Expected (%s) to be a CTCP.", yes) 57 | } 58 | if IsCTCPString(no) { 59 | t.Errorf("Expected (%s) to NOT be a CTCP.", no) 60 | } 61 | } 62 | 63 | func TestCTCPUnpack(t *testing.T) { 64 | in := []byte("\x01\x10\r\x10\n\x10\x10 \x5Ca\x5C\x5C\x01") 65 | expect1 := []byte("\r\n\x10") 66 | expect2 := []byte("\x01\x5C") 67 | 68 | out1, out2 := CTCPunpack(in) 69 | if 0 != bytes.Compare(out1, expect1) { 70 | t.Errorf("1: Expected: [% X] Got: [% X]", expect1, out1) 71 | } 72 | if 0 != bytes.Compare(out2, expect2) { 73 | t.Errorf("2: Expected: [% X] Got: [% X]", expect2, out2) 74 | } 75 | } 76 | 77 | func TestCTCPPack(t *testing.T) { 78 | in1 := []byte("\r\n\x10") 79 | in2 := []byte("\x01\x5C") 80 | expect := []byte("\x01\x10\r\x10\n\x10\x10 \x5Ca\x5C\x5C\x01") 81 | 82 | out := CTCPpack(in1, in2) 83 | if 0 != bytes.Compare(out, expect) { 84 | t.Errorf("Expected: [% X] Got: [% X]", expect, out) 85 | } 86 | } 87 | 88 | func TestCTCPUnpackString(t *testing.T) { 89 | in := "\x01DCC SEND moozic.txt 1122250358 37294 130\x01" 90 | expect1 := "DCC" 91 | expect2 := "SEND moozic.txt 1122250358 37294 130" 92 | 93 | out1, out2 := CTCPunpackString(in) 94 | if out1 != expect1 { 95 | t.Errorf("1: Expected: [%s] Got: [%s]", expect1, out1) 96 | } 97 | if out2 != expect2 { 98 | t.Errorf("2: Expected: [%s] Got: [%s]", expect2, out2) 99 | } 100 | } 101 | 102 | func TestCTCPPackString(t *testing.T) { 103 | in1 := "DCC" 104 | in2 := "SEND moozic.txt 1122250358 37294 130" 105 | expect := "\x01DCC SEND moozic.txt 1122250358 37294 130\x01" 106 | 107 | out := CTCPpackString(in1, in2) 108 | if out != expect { 109 | t.Errorf("Expected: [%s] Got: [%s]", expect, out) 110 | } 111 | } 112 | 113 | func TestCTCPunpack(t *testing.T) { 114 | in := []byte("a b c d") 115 | expect1 := []byte("a") 116 | expect2 := []byte("b c d") 117 | 118 | out1, out2 := ctcpUnpack(in) 119 | if 0 != bytes.Compare(out1, expect1) { 120 | t.Errorf("1: Expected: [% X] Got: [% X]", expect1, out1) 121 | } 122 | if 0 != bytes.Compare(out2, expect2) { 123 | t.Errorf("2: Expected: [% X] Got: [% X]", expect2, out2) 124 | } 125 | 126 | in = []byte("abcd") 127 | expect1 = in 128 | out1, out2 = ctcpUnpack(in) 129 | if 0 != bytes.Compare(out1, expect1) { 130 | t.Errorf("1: Expected: [% X] Got: [% X]", expect1, out1) 131 | } 132 | if out2 != nil { 133 | t.Errorf("2: Expected data to be nil, was: [% X]", out2) 134 | } 135 | } 136 | 137 | func TestCTCPpack(t *testing.T) { 138 | in1 := []byte("a") 139 | in2 := []byte("b c d") 140 | expect := []byte("a b c d") 141 | 142 | out := ctcpPack(in1, in2) 143 | if 0 != bytes.Compare(out, expect) { 144 | t.Errorf("1: Expected: [% X] Got: [% X]", expect, out) 145 | } 146 | 147 | in1 = []byte("abcd") 148 | in2 = []byte{} 149 | expect = in1 150 | out = ctcpPack(in1, in2) 151 | if 0 != bytes.Compare(out, expect) { 152 | t.Errorf("1: Expected: [% X] Got: [% X]", expect, out) 153 | } 154 | 155 | in2 = nil 156 | out = ctcpPack(in1, in2) 157 | if 0 != bytes.Compare(out, expect) { 158 | t.Errorf("1: Expected: [% X] Got: [% X]", expect, out) 159 | } 160 | } 161 | 162 | func TestCTCPHighLevelEscape(t *testing.T) { 163 | in := []byte("\x01\x5C") 164 | expect := []byte("\x5Ca\x5C\x5C") 165 | 166 | if out := ctcpHighLevelEscape(in); 0 != bytes.Compare(out, expect) { 167 | t.Errorf("Expected: [% X] Got: [% X]", expect, out) 168 | } 169 | } 170 | 171 | func TestCTCPHighLevelUnescape(t *testing.T) { 172 | in := []byte("\x5Ca\x5C\x5C") 173 | expect := []byte("\x01\x5C") 174 | 175 | if out := ctcpHighLevelUnescape(in); 0 != bytes.Compare(out, expect) { 176 | t.Errorf("Expected: [% X] Got: [% X]", expect, out) 177 | } 178 | } 179 | 180 | func TestCTCPLowLevelEscape(t *testing.T) { 181 | in := []byte("\n\r\x00\x10") 182 | expect := []byte("\x10\n\x10\r\x10\x00\x10\x10") 183 | 184 | if out := ctcpLowLevelEscape(in); 0 != bytes.Compare(out, expect) { 185 | t.Errorf("Expected: [% X] Got: [% X]", expect, out) 186 | } 187 | } 188 | 189 | func TestCTCPLowLevelUnescape(t *testing.T) { 190 | in := []byte("\x10\n\x10\r\x10\x00\x10\x10") 191 | expect := []byte("\n\r\x00\x10") 192 | 193 | if out := ctcpLowLevelUnescape(in); 0 != bytes.Compare(out, expect) { 194 | t.Errorf("Expected: [% X] Got: [% X]", expect, out) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /irc/hosts_test.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHost(t *testing.T) { 8 | var host Host = "nick!user@host" 9 | 10 | if s := host.Nick(); s != "nick" { 11 | t.Errorf("Expected: nick, got: %s", s) 12 | } 13 | if s := host.Username(); s != "user" { 14 | t.Errorf("Expected: user, got: %s", s) 15 | } 16 | if s := host.Hostname(); s != "host" { 17 | t.Errorf("Expected: host, got: %s", s) 18 | } 19 | if s := host.String(); s != string(host) { 20 | t.Errorf("Expected: %v, got: %s", string(host), s) 21 | } 22 | 23 | host = "nick@user!host" 24 | if s := host.Nick(); s != "nick" { 25 | t.Errorf("Expected: nick, got: %s", s) 26 | } 27 | if s := host.Username(); len(s) != 0 { 28 | t.Errorf("Expected: empty string, got: %s", s) 29 | } 30 | if s := host.Hostname(); len(s) != 0 { 31 | t.Errorf("Expected: empty string, got: %s", s) 32 | } 33 | if s := host.String(); s != string(host) { 34 | t.Errorf("Expected: %v, got: %s", string(host), s) 35 | } 36 | 37 | host = "nick" 38 | if s := host.Nick(); s != "nick" { 39 | t.Errorf("Expected: nick, got: %s", s) 40 | } 41 | if s := host.Username(); len(s) != 0 { 42 | t.Errorf("Expected: empty string, got: %s", s) 43 | } 44 | if s := host.Hostname(); len(s) != 0 { 45 | t.Errorf("Expected: empty string, got: %s", s) 46 | } 47 | if s := host.String(); s != string(host) { 48 | t.Errorf("Expected: %v, got: %s", string(host), s) 49 | } 50 | } 51 | 52 | func TestHost_SplitHost(t *testing.T) { 53 | var nick, user, hostname string 54 | 55 | nick, user, hostname = Host("nick!user@host").Split() 56 | if s := nick; s != "nick" { 57 | t.Errorf("Expected: nick, got: %s", s) 58 | } 59 | if s := user; s != "user" { 60 | t.Errorf("Expected: user, got: %s", s) 61 | } 62 | if s := hostname; s != "host" { 63 | t.Errorf("Expected: host, got: %s", s) 64 | } 65 | 66 | nick, user, hostname = Host("ni ck!user@host").Split() 67 | if s := nick; len(s) != 0 { 68 | t.Errorf("Expected: empty string, got: %s", s) 69 | } 70 | if s := user; len(s) != 0 { 71 | t.Errorf("Expected: empty string, got: %s", s) 72 | } 73 | if s := hostname; len(s) != 0 { 74 | t.Errorf("Expected: empty string, got: %s", s) 75 | } 76 | } 77 | 78 | func TestHost_IsValid(t *testing.T) { 79 | tests := []struct { 80 | Host Host 81 | IsValid bool 82 | }{ 83 | {"", false}, 84 | {"!@", false}, 85 | {"nick", false}, 86 | {"nick!", false}, 87 | {"nick@", false}, 88 | {"nick@host!user", false}, 89 | {"nick!user@host", true}, 90 | } 91 | 92 | for _, test := range tests { 93 | if result := test.Host.IsValid(); result != test.IsValid { 94 | t.Errorf("Expected '%v'.IsValid() to be %v.", test.Host, test.IsValid) 95 | } 96 | } 97 | } 98 | 99 | func TestMask_Split(t *testing.T) { 100 | var nick, user, host string 101 | nick, user, host = Mask("n?i*ck!u*ser@h*o?st").Split() 102 | if s := nick; s != "n?i*ck" { 103 | t.Errorf("Expected: n?i*ck, got: %s", s) 104 | } 105 | if s := user; s != "u*ser" { 106 | t.Errorf("Expected: u*ser, got: %s", s) 107 | } 108 | if s := host; s != "h*o?st" { 109 | t.Errorf("Expected: h*o?st, got: %s", s) 110 | } 111 | 112 | nick, user, host = Mask("n?i* ck!u*ser@h*o?st").Split() 113 | if s := nick; len(s) != 0 { 114 | t.Errorf("Expected: empty string, got: %s", s) 115 | } 116 | if s := user; len(s) != 0 { 117 | t.Errorf("Expected: empty string, got: %s", s) 118 | } 119 | if s := host; len(s) != 0 { 120 | t.Errorf("Expected: empty string, got: %s", s) 121 | } 122 | } 123 | 124 | func TestMask_IsValid(t *testing.T) { 125 | tests := []struct { 126 | Mask Mask 127 | IsValid bool 128 | }{ 129 | {"", false}, 130 | {"!@", false}, 131 | {"n?i*ck", false}, 132 | {"n?i*ck!", false}, 133 | {"n?i*ck@", false}, 134 | {"n*i?ck@h*o?st!u*ser", false}, 135 | {"n?i*ck!u*ser@h*o?st", true}, 136 | } 137 | 138 | for _, test := range tests { 139 | if result := test.Mask.IsValid(); result != test.IsValid { 140 | t.Errorf("Expected '%v'.IsValid() to be %v.", 141 | test.Mask, test.IsValid) 142 | } 143 | } 144 | } 145 | 146 | func TestMask_Match(t *testing.T) { 147 | var mask Mask 148 | var host Host 149 | if !mask.Match(host) { 150 | t.Error("Expected empty case to evaluate true.") 151 | } 152 | 153 | if !Mask("nick!*@*").Match("nick!@") { 154 | t.Error("Expected trivial case to evaluate true.") 155 | } 156 | 157 | host = "nick!user@host" 158 | 159 | positiveMasks := []Mask{ 160 | // Default 161 | `nick!user@host`, 162 | // *'s 163 | `*`, `*!*@*`, `**!**@**`, `*@host`, `**@host`, 164 | `nick!*`, `nick!**`, `*nick!user@host`, `**nick!user@host`, 165 | `nick!user@host*`, `nick!user@host**`, 166 | // ?'s 167 | `ni?k!us?r@ho?st`, `ni??k!us??r@ho??st`, `????!????@????`, 168 | `?ick!user@host`, `??ick!user@host`, `?nick!user@host`, 169 | `??nick!user@host`, `nick!user@hos?`, `nick!user@hos??`, 170 | `nick!user@host?`, `nick!user@host??`, 171 | // Combination 172 | `?*nick!user@host`, `*?nick!user@host`, `??**nick!user@host`, 173 | `**??nick!user@host`, 174 | `nick!user@host?*`, `nick!user@host*?`, `nick!user@host??**`, 175 | `nick!user@host**??`, `nick!u?*?ser@host`, `nick!u?*?ser@host`, 176 | } 177 | 178 | for i := 0; i < len(positiveMasks); i++ { 179 | if !positiveMasks[i].Match(host) { 180 | t.Errorf("Expected: %v to match %v", positiveMasks[i], host) 181 | } 182 | if !host.Match(positiveMasks[i]) { 183 | t.Errorf("Expected: %v to match %v", host, positiveMasks[i]) 184 | } 185 | } 186 | 187 | negativeMasks := []Mask{ 188 | ``, `?nq******c?!*@*`, `nick2!*@*`, `*!*@hostfail`, `*!*@failhost`, 189 | } 190 | 191 | for i := 0; i < len(negativeMasks); i++ { 192 | if negativeMasks[i].Match(host) { 193 | t.Errorf("Expected: %v not to match %v", negativeMasks[i], host) 194 | } 195 | if host.Match(negativeMasks[i]) { 196 | t.Errorf("Expected: %v to match %v", host, negativeMasks[i]) 197 | } 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /config/map_helpers.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // mp is used to provide helper methods on the map type we use most often 4 | // this cleans up a lot of excessive type assertion stuff. 5 | type mp map[string]interface{} 6 | 7 | func intfToMp(intf interface{}) mp { 8 | if m, ok := intf.(map[string]interface{}); ok { 9 | return m 10 | } 11 | return nil 12 | } 13 | 14 | func (m mp) get(name string) mp { 15 | if m == nil { 16 | return nil 17 | } 18 | 19 | if mpVal, ok := m[name]; ok { 20 | switch v := mpVal.(type) { 21 | case map[string]interface{}: 22 | return v 23 | case mp: 24 | return v 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func (m mp) ensure(name string) mp { 32 | if m == nil { 33 | return nil 34 | } 35 | 36 | if mpVal, ok := m[name]; ok { 37 | switch v := mpVal.(type) { 38 | case map[string]interface{}: 39 | return v 40 | case mp: 41 | return v 42 | default: 43 | return nil 44 | } 45 | } else { 46 | made := map[string]interface{}{} 47 | m[name] = made 48 | return made 49 | } 50 | } 51 | 52 | func (m mp) getArr(name string) []map[string]interface{} { 53 | if m == nil { 54 | return nil 55 | } 56 | 57 | if mpVal, ok := m[name]; ok { 58 | switch v := mpVal.(type) { 59 | case []map[string]interface{}: 60 | return v 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func copyMap(dest, src mp) { 68 | for key, value := range src { 69 | switch v := value.(type) { 70 | case map[string]interface{}: 71 | child := make(map[string]interface{}) 72 | dest[key] = child 73 | copyMap(child, v) 74 | case mp: 75 | child := make(map[string]interface{}) 76 | dest[key] = child 77 | copyMap(child, v) 78 | 79 | // because we only use string or channel arrays, both of which are 80 | // not holding reference types, these naive array copies should be ok. 81 | case []interface{}: 82 | intfArr := make([]interface{}, len(v)) 83 | copy(intfArr, v) 84 | dest[key] = intfArr 85 | case []map[string]interface{}: 86 | mapArr := make([]map[string]interface{}, len(v)) 87 | copy(mapArr, v) 88 | dest[key] = mapArr 89 | for i, srcMap := range v { 90 | copyMap(mapArr[i], srcMap) 91 | } 92 | case []string: 93 | strArr := make([]string, len(v)) 94 | copy(strArr, v) 95 | dest[key] = strArr 96 | case []Channel: 97 | chans := make([]Channel, len(v)) 98 | copy(chans, v) 99 | dest[key] = chans 100 | default: 101 | dest[key] = v 102 | } 103 | } 104 | } 105 | 106 | type mapGetter interface { 107 | get(string) (interface{}, bool) 108 | getParent(string) (interface{}, bool) 109 | rlock() 110 | runlock() 111 | } 112 | 113 | type mapSetter interface { 114 | set(string, interface{}) 115 | lock() 116 | unlock() 117 | } 118 | 119 | type mapGetSetter interface { 120 | mapGetter 121 | mapSetter 122 | } 123 | 124 | func setVal(m mapSetter, key string, value interface{}) { 125 | m.lock() 126 | m.set(key, value) 127 | m.unlock() 128 | } 129 | 130 | // getStr gets a string out of a map. 131 | func getStr(m mapGetter, key string, fallback bool) (string, bool) { 132 | m.rlock() 133 | defer m.runlock() 134 | 135 | var val interface{} 136 | var ok bool 137 | 138 | if val, ok = m.get(key); !ok && fallback { 139 | val, ok = m.getParent(key) 140 | } 141 | 142 | if !ok { 143 | return "", false 144 | } 145 | 146 | if str, ok := val.(string); ok { 147 | return str, true 148 | } 149 | 150 | return "", false 151 | } 152 | 153 | // getBool gets a bool out of a map. 154 | func getBool(m mapGetter, key string, fallback bool) (bool, bool) { 155 | m.rlock() 156 | defer m.runlock() 157 | 158 | var val interface{} 159 | var ok bool 160 | 161 | if val, ok = m.get(key); !ok && fallback { 162 | val, ok = m.getParent(key) 163 | } 164 | 165 | if !ok { 166 | return false, false 167 | } 168 | 169 | if boolval, ok := val.(bool); ok { 170 | return boolval, true 171 | } 172 | 173 | return false, false 174 | } 175 | 176 | // getUint gets a bool out of a map. 177 | func getUint(m mapGetter, key string, fallback bool) (uint, bool) { 178 | m.rlock() 179 | defer m.runlock() 180 | 181 | var val interface{} 182 | var ok bool 183 | 184 | if val, ok = m.get(key); !ok && fallback { 185 | val, ok = m.getParent(key) 186 | } 187 | 188 | if !ok { 189 | return 0, false 190 | } 191 | 192 | switch v := val.(type) { 193 | case int64: // After a toml parse. 194 | return uint(v), true 195 | case uint: // After a set. 196 | return v, true 197 | } 198 | 199 | return 0, false 200 | } 201 | 202 | // getFloat64 gets a bool out of a map. 203 | func getFloat64(m mapGetter, key string, fallback bool) (float64, bool) { 204 | m.rlock() 205 | defer m.runlock() 206 | 207 | var val interface{} 208 | var ok bool 209 | 210 | if val, ok = m.get(key); !ok && fallback { 211 | val, ok = m.getParent(key) 212 | } 213 | 214 | if !ok { 215 | return 0, false 216 | } 217 | 218 | if float, ok := val.(float64); ok { 219 | return float, true 220 | } 221 | 222 | return 0, false 223 | } 224 | 225 | // getStrArr gets a string array out of a map. 226 | func getStrArr(m mapGetter, key string, fallback bool) ([]string, bool) { 227 | m.rlock() 228 | defer m.runlock() 229 | 230 | var val interface{} 231 | var ok bool 232 | 233 | if val, ok = m.get(key); !ok && fallback { 234 | val, ok = m.getParent(key) 235 | } 236 | 237 | if !ok { 238 | return nil, false 239 | } 240 | 241 | if arr, ok := val.([]interface{}); ok { 242 | if len(arr) == 0 { 243 | return nil, false 244 | } 245 | 246 | cpyArr := make([]string, 0, len(arr)) 247 | for _, strval := range arr { 248 | if str, ok := strval.(string); ok { 249 | cpyArr = append(cpyArr, str) 250 | } 251 | } 252 | 253 | return cpyArr, true 254 | } else if arr, ok := val.([]string); ok { 255 | if len(arr) == 0 { 256 | return nil, false 257 | } 258 | 259 | cpyArr := make([]string, len(arr)) 260 | copy(cpyArr, arr) 261 | return cpyArr, true 262 | } 263 | 264 | return nil, false 265 | } 266 | -------------------------------------------------------------------------------- /dispatch/cmd/command.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/aarondl/ultimateq/irc" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | rgxCmd = regexp.MustCompile(`^[a-z][a-z0-9]*$`) 13 | ) 14 | 15 | // Handler for command types 16 | type Handler interface { 17 | Cmd(name string, writer irc.Writer, event *Event) error 18 | } 19 | 20 | // HandlerFunc implements Handler 21 | type HandlerFunc func(name string, writer irc.Writer, event *Event) error 22 | 23 | // Cmd implements Handler 24 | func (h HandlerFunc) Cmd(name string, writer irc.Writer, event *Event) error { 25 | return h(name, writer, event) 26 | } 27 | 28 | // Command holds all the information about a command. 29 | type Command struct { 30 | // The name of the command. 31 | Name string 32 | // Extension is the name of the extension registering this command. 33 | Extension string 34 | // Description is a description of the command's function. 35 | Description string 36 | // Kind is the kind of messages this command reacts to, may be 37 | // any of the constants: Privmsg, Notice or AllKinds. 38 | Kind Kind 39 | // Scope is the scope of the messages this command reacts to, may be 40 | // any of the constants: Private, Public or Allscopes. 41 | Scope Scope 42 | // Args is the arguments for the command. Each argument must be in it's own 43 | // element, be named with flags optionally prefixing the name, and have the 44 | // form of one of the following: 45 | // #channel: This form is for requiring a target channel for the command. 46 | // If this parameter is present and a message directly to the bot is 47 | // received this parameter is required and if it's missing an error 48 | // will be returned. 49 | // If this parameter is present and a message to a channel is received 50 | // the there is two cases: 1) The first parameter given is a channel, 51 | // this then becomes the TargetChannel. 2) The first parameter given 52 | // is non existent or not a channel, the current channel then becomes 53 | // the TargetChannel. 54 | // required: This form marks a required attribute and it must be present 55 | // or an error will be returned. It must come after #channel but before 56 | // [optional] and varargs... arguments. 57 | // [optional]: This form is an optional argument. It must come before after 58 | // required but before varargs... arguments. 59 | // varargs...: This form is a variadic argument, there may be 0 or more 60 | // arguments to satisfy this parameter and they will all be parsed 61 | // together as one string by the commander. This must come at the end. 62 | // There are two types of flags available: 63 | // ~: This flag is a nickname flag. If this flag is present the bot 64 | // will look up the nickname given in the state database, if it does 65 | // not exist an error will occur. 66 | // *: This flag is a user flag. It looks up a user based on nick OR 67 | // username. If any old nickname is given, it first looks up the user 68 | // in the state database, and then checks his authentication record 69 | // to get his username (and therefore access). If the name is prefixed 70 | // by a *, then it looks up the user based on username directly. If 71 | // the user is not found (via nickname), not authed (via username) 72 | // the command will fail. 73 | Args []string 74 | // RequireAuth is whether or not this command requires authentication. 75 | RequireAuth bool 76 | // ReqLevel is the required level for use. 77 | ReqLevel uint8 78 | // ReqFlags is the required flags for use. 79 | ReqFlags string 80 | // Handler the handler structure that will handle events for this command. 81 | Handler Handler 82 | 83 | parsedArgs commandArgs 84 | } 85 | 86 | // New is a helper method to easily create a Command. See the documentation 87 | // for Command on what each parameter is. Panics if the args are invalid. 88 | func New( 89 | ext, 90 | cmd, 91 | desc string, 92 | handler Handler, 93 | kind Kind, 94 | scope Scope, 95 | args ...string) *Command { 96 | 97 | command, err := NewErr(ext, cmd, desc, handler, kind, scope, args...) 98 | if err != nil { 99 | panic(err) 100 | } 101 | 102 | return command 103 | } 104 | 105 | // NewErr is like New but does not panic 106 | func NewErr( 107 | ext, 108 | cmd, 109 | desc string, 110 | handler Handler, 111 | kind Kind, 112 | scope Scope, 113 | args ...string) (*Command, error) { 114 | 115 | cmd = strings.ToLower(cmd) 116 | if !rgxCmd.MatchString(cmd) { 117 | return nil, errors.Errorf("command name must start with a letter, and can be followed only be letters and numbers: %s", cmd) 118 | } 119 | 120 | command := &Command{ 121 | Name: strings.ToLower(cmd), 122 | Extension: strings.ToLower(ext), 123 | Description: desc, 124 | Handler: handler, 125 | Kind: kind, 126 | Scope: scope, 127 | Args: args, 128 | } 129 | 130 | if err := command.parseArgs(); err != nil { 131 | return nil, err 132 | } 133 | 134 | return command, nil 135 | } 136 | 137 | // NewAuthed is a helper method to easily create an authenticated Command. See 138 | // the documentation on Command for what each parameter is. 139 | // Panics if the args are invalid. 140 | func NewAuthed( 141 | ext, 142 | cmd, 143 | desc string, 144 | handler Handler, 145 | kind Kind, 146 | scope Scope, 147 | reqLevel uint8, 148 | reqFlags string, 149 | args ...string) *Command { 150 | 151 | command, err := NewAuthedErr(ext, cmd, desc, handler, kind, scope, reqLevel, reqFlags, args...) 152 | if err != nil { 153 | panic(err) 154 | } 155 | 156 | return command 157 | } 158 | 159 | // NewAuthedErr is the same as NewAuthed but returns an error instead of panics 160 | func NewAuthedErr( 161 | ext, 162 | cmd, 163 | desc string, 164 | handler Handler, 165 | kind Kind, 166 | scope Scope, 167 | reqLevel uint8, 168 | reqFlags string, 169 | args ...string) (*Command, error) { 170 | 171 | command, err := NewErr(ext, cmd, desc, handler, kind, scope, args...) 172 | if err != nil { 173 | return nil, err 174 | } 175 | command.RequireAuth = true 176 | command.ReqLevel = reqLevel 177 | command.ReqFlags = reqFlags 178 | 179 | return command, nil 180 | } 181 | -------------------------------------------------------------------------------- /bot/server_test.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "testing" 8 | 9 | "github.com/aarondl/ultimateq/irc" 10 | "github.com/aarondl/ultimateq/mocks" 11 | ) 12 | 13 | func TestServer_createIrcClient(t *testing.T) { 14 | t.Parallel() 15 | errch := make(chan error) 16 | connProvider := func(srv string) (net.Conn, error) { 17 | return nil, nil 18 | } 19 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, false, false) 20 | srv := b.servers[netID] 21 | 22 | go func() { 23 | err, _ := srv.createIrcClient() 24 | errch <- err 25 | }() 26 | 27 | if <-errch != nil { 28 | t.Error("Expected a clean connect.") 29 | } 30 | if srv.client == nil { 31 | t.Error("Client should have been instantiated.") 32 | } 33 | } 34 | 35 | func TestServer_createIrcClient_failConn(t *testing.T) { 36 | t.Parallel() 37 | errch := make(chan error) 38 | connProvider := func(srv string) (net.Conn, error) { 39 | return nil, io.EOF 40 | } 41 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, false, false) 42 | srv := b.servers[netID] 43 | 44 | go func() { 45 | err, _ := srv.createIrcClient() 46 | errch <- err 47 | }() 48 | 49 | if <-errch != io.EOF { 50 | t.Error("Expected a failed connection.") 51 | } 52 | } 53 | 54 | func TestServer_createIrcClient_killConn(t *testing.T) { 55 | t.Parallel() 56 | errch := make(chan error) 57 | connCh := make(chan int) 58 | connProvider := func(srv string) (net.Conn, error) { 59 | <-connCh 60 | return nil, io.EOF 61 | } 62 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, false, false) 63 | srv := b.servers[netID] 64 | srv.killable = make(chan int) 65 | 66 | go func() { 67 | err, _ := srv.createIrcClient() 68 | errch <- err 69 | }() 70 | 71 | close(srv.killable) 72 | if <-errch != errServerKilledConn { 73 | t.Error("Expected a killed connection.") 74 | } 75 | 76 | close(connCh) 77 | } 78 | 79 | /* 80 | func TestServer_createTlsConfig(t *testing.T) { 81 | t.Parallel() 82 | b, _ := createBot(fakeConfig, nil, nil, devNull, false, false) 83 | srv := b.servers[netID] 84 | 85 | pool := x509.NewCertPool() 86 | tlsConfig, _ := srv.createTlsConfig(func(_ string) (*x509.CertPool, error) { 87 | return pool, nil 88 | }) 89 | 90 | if !tlsConfig.InsecureSkipVerify { 91 | t.Error("This should have been set to fakeconfig's value.") 92 | } 93 | if tlsConfig.RootCAs != pool { 94 | t.Error("The provided root ca pool should be used.") 95 | } 96 | } 97 | */ 98 | 99 | func TestServer_Close(t *testing.T) { 100 | t.Parallel() 101 | errch := make(chan error) 102 | conn := mocks.NewConn() 103 | connProvider := func(srv string) (net.Conn, error) { 104 | return conn, nil 105 | } 106 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, false, false) 107 | srv := b.servers[netID] 108 | 109 | go func() { 110 | err, _ := srv.createIrcClient() 111 | errch <- err 112 | }() 113 | 114 | if err := <-errch; err != nil { 115 | t.Error("Unexpected:", err) 116 | } 117 | 118 | if err := srv.Close(); err != nil { 119 | t.Error("Unexpected:", err) 120 | } 121 | 122 | if srv.client != nil { 123 | t.Error("Expected client to be nil.") 124 | } 125 | } 126 | 127 | func TestServer_Status(t *testing.T) { 128 | t.Parallel() 129 | srv := &Server{} 130 | 131 | status := make(chan Status) 132 | connAndStop := make(chan Status) 133 | srv.addStatusListener(connAndStop, STATUS_CONNECTING, STATUS_STOPPED) 134 | srv.addStatusListener(status) 135 | 136 | done := make(chan int) 137 | 138 | go func() { 139 | srv.setStatus(STATUS_CONNECTING) 140 | srv.setStatus(STATUS_STARTED) 141 | srv.setStatus(STATUS_STOPPED) 142 | }() 143 | 144 | go func() { 145 | ers := 0 146 | if st := <-status; st != STATUS_CONNECTING { 147 | t.Error("Received the wrong state:", st) 148 | ers++ 149 | } 150 | if st := <-status; st != STATUS_STARTED { 151 | t.Error("Received the wrong state:", st) 152 | ers++ 153 | } 154 | if st := <-status; st != STATUS_STOPPED { 155 | t.Error("Received the wrong state:", st) 156 | ers++ 157 | } 158 | done <- ers 159 | }() 160 | 161 | go func() { 162 | ers := 0 163 | if st := <-connAndStop; st != STATUS_CONNECTING { 164 | t.Error("Received the wrong state:", st) 165 | ers++ 166 | } 167 | if st := <-connAndStop; st != STATUS_STOPPED { 168 | t.Error("Received the wrong state:", st) 169 | ers++ 170 | } 171 | done <- ers 172 | }() 173 | 174 | if ers := <-done; ers > 0 { 175 | t.Error(ers, " errors encountered during run.") 176 | } 177 | if ers := <-done; ers > 0 { 178 | t.Error(ers, " errors encountered during run.") 179 | } 180 | } 181 | 182 | func TestServer_rehashNetworkInfo(t *testing.T) { 183 | t.Parallel() 184 | b, _ := createBot(fakeConfig, nil, nil, devNull, false, false) 185 | srv := b.servers[netID] 186 | 187 | srv.netInfo.ParseISupport(&irc.Event{Args: []string{ 188 | "NICK", "CHANTYPES=@", 189 | }}) 190 | err := srv.rehashNetworkInfo() 191 | if err != nil { 192 | t.Error("Unexpected:", err) 193 | } 194 | 195 | if srv.netInfo.Chantypes() != "@" { 196 | t.Error("Protocaps were not set by rehash.") 197 | } 198 | } 199 | 200 | func TestServer_Write(t *testing.T) { 201 | t.Parallel() 202 | conn := mocks.NewConn() 203 | connProvider := func(srv string) (net.Conn, error) { 204 | return conn, nil 205 | } 206 | 207 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, false, false) 208 | srv := b.servers[netID] 209 | 210 | var err error 211 | _, err = srv.Write(nil) 212 | if err != nil { 213 | t.Error("Expected:", err) 214 | } 215 | _, err = srv.Write([]byte{1}) 216 | if err != errNotConnected { 217 | t.Error("Expected:", errNotConnected, "got:", err) 218 | } 219 | 220 | listen := make(chan Status) 221 | srv.addStatusListener(listen, STATUS_STARTED) 222 | 223 | end := b.Start() 224 | 225 | for <-listen != STATUS_STARTED { 226 | } 227 | 228 | message := []byte("PONG :msg\r\n") 229 | if _, err = srv.Write(message); err != nil { 230 | t.Error("Unexpected write error:", err) 231 | } 232 | got := conn.Receive(len(message), nil) 233 | if bytes.Compare(got, message) != 0 { 234 | t.Errorf("Socket received wrong message: (%s) != (%s)", got, message) 235 | } 236 | 237 | b.Stop() 238 | for range end { 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /data/access_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestAccess(t *testing.T) { 9 | t.Parallel() 10 | a := NewAccess(0) 11 | if a == nil { 12 | t.Fatal("Failed to Create") 13 | } 14 | if a.Level != 0 || a.Flags != 0 { 15 | t.Error("Bad init") 16 | } 17 | 18 | a = NewAccess(100, "aBC", "d") 19 | if a.Level != 100 { 20 | t.Error("Level was not set") 21 | } 22 | for _, v := range "aBCd" { 23 | if !a.HasFlag(v) { 24 | t.Errorf("Flag %c was not found.", v) 25 | } 26 | } 27 | } 28 | 29 | func TestAccess_HasLevel(t *testing.T) { 30 | t.Parallel() 31 | a := NewAccess(50) 32 | 33 | var table = map[uint8]bool{ 34 | 50: true, 35 | 49: true, 36 | 51: false, 37 | } 38 | 39 | for level, result := range table { 40 | if res := a.HasLevel(level); res != result { 41 | t.Errorf("HasLevel %v resulted in: %v", level, res) 42 | } 43 | } 44 | } 45 | 46 | func TestAccess_HasFlag(t *testing.T) { 47 | t.Parallel() 48 | a := NewAccess(0, "aBC", "d") 49 | for _, v := range "aBCd" { 50 | if !a.HasFlag(v) { 51 | t.Errorf("Flag %c was not found.", v) 52 | } 53 | } 54 | } 55 | 56 | func TestAccess_HasFlags(t *testing.T) { 57 | t.Parallel() 58 | a := NewAccess(0, "aBC", "d") 59 | if !a.HasFlags("aBCd") { 60 | t.Error("Flags were not all found.") 61 | } 62 | if !a.HasFlags("aZ") || !a.HasFlags("zB") { 63 | t.Error("Flags should or together for access.") 64 | } 65 | } 66 | 67 | func TestAccess_SetFlag(t *testing.T) { 68 | t.Parallel() 69 | a := NewAccess(0) 70 | if a.HasFlag('a') { 71 | t.Error("Really bad init.") 72 | } 73 | a.SetFlag('a') 74 | if !a.HasFlag('a') { 75 | t.Error("Set flag failed") 76 | } 77 | a.SetFlag('A') 78 | if !a.HasFlag('A') { 79 | t.Error("Set flag failed") 80 | } 81 | a.SetFlag('!') 82 | if !a.HasFlag('a') || !a.HasFlag('A') || a.HasFlag('!') { 83 | t.Error("Set flag failed") 84 | } 85 | } 86 | 87 | func TestAccess_MultiEffects(t *testing.T) { 88 | t.Parallel() 89 | a := NewAccess(0) 90 | a.SetFlags("ab", "A") 91 | if !a.HasFlags("ab", "A") { 92 | t.Error("Set flags failed") 93 | } 94 | a.ClearFlags("ab", "A") 95 | if a.HasFlags("a") || a.HasFlags("b") || a.HasFlags("A") { 96 | t.Error("Clear flags failed") 97 | } 98 | } 99 | 100 | func TestAccess_ClearFlag(t *testing.T) { 101 | t.Parallel() 102 | a := NewAccess(0, "aBCd") 103 | for _, v := range "aBCd" { 104 | if !a.HasFlag(v) { 105 | t.Errorf("Flag %c was not found.", v) 106 | } 107 | } 108 | 109 | a.ClearFlag('a') 110 | a.ClearFlag('C') 111 | 112 | for _, v := range "Bd" { 113 | if !a.HasFlag(v) { 114 | t.Errorf("Flag %c was not found.", v) 115 | } 116 | } 117 | for _, v := range "aC" { 118 | if a.HasFlag(v) { 119 | t.Errorf("Flag %c was found.", v) 120 | } 121 | } 122 | } 123 | 124 | func TestAccess_ClearAllFlags(t *testing.T) { 125 | t.Parallel() 126 | a := NewAccess(0, "aBCd") 127 | a.ClearAllFlags() 128 | 129 | for _, v := range "aBCd" { 130 | if a.HasFlag(v) { 131 | t.Errorf("Flag %c was found.", v) 132 | } 133 | } 134 | } 135 | 136 | func TestAccess_IsZero(t *testing.T) { 137 | t.Parallel() 138 | a := Access{} 139 | if !a.IsZero() { 140 | t.Error("Should be zero.") 141 | } 142 | a.SetAccess(1, "a") 143 | if a.IsZero() { 144 | t.Error("Should not be zero.") 145 | } 146 | } 147 | 148 | func TestAccess_String(t *testing.T) { 149 | t.Parallel() 150 | 151 | var table = []struct { 152 | Level uint8 153 | Flags string 154 | Expect string 155 | }{ 156 | {100, "aBCd", "100 BCad"}, 157 | {0, wholeAlphabet, allFlags}, 158 | {0, "BCad", "BCad"}, 159 | {100, "", "100"}, 160 | {0, "", none}, 161 | } 162 | 163 | for _, test := range table { 164 | a := NewAccess(test.Level, test.Flags) 165 | if was := a.String(); was != test.Expect { 166 | t.Errorf("Expected: %s, was: %s", test.Expect, was) 167 | } 168 | } 169 | } 170 | 171 | func Test_getFlagBits(t *testing.T) { 172 | t.Parallel() 173 | bits := getFlagBits("Aab") 174 | aFlag, bFlag, AFlag := getFlagBit('a'), getFlagBit('b'), getFlagBit('A') 175 | if aFlag != aFlag&bits { 176 | t.Error("The correct bit was not set.") 177 | } 178 | if bFlag != bFlag&bits { 179 | t.Error("The correct bit was not set.") 180 | } 181 | if AFlag != AFlag&bits { 182 | t.Error("The correct bit was not set.") 183 | } 184 | } 185 | 186 | func Test_getFlagBit(t *testing.T) { 187 | t.Parallel() 188 | var table = map[rune]uint64{ 189 | 'A': 0x1, 190 | 'Z': 0x1 << (nAlphabet - 1), 191 | 'a': 0x1 << nAlphabet, 192 | 'z': 0x1 << (nAlphabet*2 - 1), 193 | '!': 0x0, '_': 0x0, '|': 0x0, 194 | } 195 | 196 | for flag, expect := range table { 197 | if bit := getFlagBit(flag); bit != expect { 198 | t.Errorf("Flag did not match: %c, %X (%X)", 199 | flag, expect, bit) 200 | } 201 | } 202 | } 203 | 204 | func Test_getFlagString(t *testing.T) { 205 | t.Parallel() 206 | var table = map[uint64]string{ 207 | 0x1: "A", 208 | 0x1 << (nAlphabet - 1): "Z", 209 | 0x1 << nAlphabet: "a", 210 | 0x1 << (nAlphabet*2 - 1): "z", 211 | } 212 | 213 | for bit, expect := range table { 214 | if flag := getFlagString(bit); flag != expect { 215 | t.Errorf("Flag did not match: %X, %s (%s)", 216 | bit, expect, flag) 217 | } 218 | } 219 | 220 | bits := getFlagBit('a') | getFlagBit('b') | getFlagBit('A') | 221 | 1<<(nAlphabet*2+1) 222 | should := "Aab" 223 | if was := getFlagString(bits); was != should { 224 | t.Errorf("Flag string should be: (%s) was: (%s)", should, was) 225 | } 226 | } 227 | 228 | func TestAccess_JSONifying(t *testing.T) { 229 | t.Parallel() 230 | 231 | a := NewAccess(23, "DEFabc") 232 | var b Access 233 | 234 | str, err := json.Marshal(a) 235 | if err != nil { 236 | t.Error(err) 237 | } 238 | 239 | if string(str) != `{"level":23,"flags":469762104}` { 240 | t.Errorf("Wrong JSON: %s", str) 241 | } 242 | 243 | if err = json.Unmarshal(str, &b); err != nil { 244 | t.Error(err) 245 | } 246 | 247 | if *a != b { 248 | t.Error("A and B differ:", a, b) 249 | } 250 | } 251 | 252 | func TestAccess_Protofy(t *testing.T) { 253 | t.Parallel() 254 | 255 | a := NewAccess(23, "DEFabc") 256 | var b Access 257 | 258 | b.FromProto(a.ToProto()) 259 | 260 | if *a != b { 261 | t.Error("A and B differ:", a, b) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /data/channel_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestChannel_Create(t *testing.T) { 10 | t.Parallel() 11 | 12 | ch := NewChannel("", testKinds) 13 | if got := ch; got != nil { 14 | t.Errorf("Expected: %v to be nil.", got) 15 | } 16 | 17 | name := "#CHAN" 18 | ch = NewChannel(name, testKinds) 19 | if ch == nil { 20 | t.Error("Unexpected nil.") 21 | } 22 | if exp, got := ch.Name, name; exp != got { 23 | t.Errorf("Expected: %v, got: %v", exp, got) 24 | } 25 | if exp, got := ch.Topic, ""; exp != got { 26 | t.Errorf("Expected: %v, got: %v", exp, got) 27 | } 28 | } 29 | 30 | func TestChannel_Bans(t *testing.T) { 31 | t.Parallel() 32 | 33 | bans := []string{"ban1", "ban2"} 34 | ch := NewChannel("name", testKinds) 35 | 36 | ch.SetBans(bans) 37 | got := ch.Bans() 38 | for i := 0; i < len(got); i++ { 39 | if exp, got := got[i], bans[i]; exp != got { 40 | t.Errorf("Expected: %v, got: %v", exp, got) 41 | } 42 | } 43 | bans[0] = "ban3" 44 | if exp, got := got[0], bans[0]; exp == got { 45 | t.Errorf("Did not want: %v, got: %v", exp, got) 46 | } 47 | 48 | if exp, got := ch.HasBan("ban2"), true; exp != got { 49 | t.Errorf("Expected: %v, got: %v", exp, got) 50 | } 51 | ch.DeleteBan("ban2") 52 | if exp, got := ch.HasBan("ban2"), false; exp != got { 53 | t.Errorf("Expected: %v, got: %v", exp, got) 54 | } 55 | 56 | if exp, got := ch.HasBan("ban2"), false; exp != got { 57 | t.Errorf("Expected: %v, got: %v", exp, got) 58 | } 59 | ch.AddBan("ban2") 60 | if exp, got := ch.HasBan("ban2"), true; exp != got { 61 | t.Errorf("Expected: %v, got: %v", exp, got) 62 | } 63 | } 64 | 65 | func TestChannel_IsBanned(t *testing.T) { 66 | t.Parallel() 67 | 68 | bans := []string{"*!*@host.com", "nick!*@*"} 69 | ch := NewChannel("name", testKinds) 70 | ch.SetBans(bans) 71 | if exp, got := ch.IsBanned("nick"), true; exp != got { 72 | t.Errorf("Expected: %v, got: %v", exp, got) 73 | } 74 | if exp, got := ch.IsBanned("notnick"), false; exp != got { 75 | t.Errorf("Expected: %v, got: %v", exp, got) 76 | } 77 | if exp, got := ch.IsBanned("nick!user@host"), true; exp != got { 78 | t.Errorf("Expected: %v, got: %v", exp, got) 79 | } 80 | if exp, got := ch.IsBanned("notnick!user@host"), false; exp != got { 81 | t.Errorf("Expected: %v, got: %v", exp, got) 82 | } 83 | if exp, got := ch.IsBanned("notnick!user@host.com"), true; exp != got { 84 | t.Errorf("Expected: %v, got: %v", exp, got) 85 | } 86 | } 87 | 88 | func TestChannel_DeleteBanWild(t *testing.T) { 89 | t.Parallel() 90 | 91 | bans := []string{"*!*@host.com", "nick!*@*", "nick2!*@*"} 92 | ch := NewChannel("name", testKinds) 93 | ch.SetBans(bans) 94 | if exp, got := ch.IsBanned("nick"), true; exp != got { 95 | t.Errorf("Expected: %v, got: %v", exp, got) 96 | } 97 | if exp, got := ch.IsBanned("notnick"), false; exp != got { 98 | t.Errorf("Expected: %v, got: %v", exp, got) 99 | } 100 | if exp, got := ch.IsBanned("nick!user@host"), true; exp != got { 101 | t.Errorf("Expected: %v, got: %v", exp, got) 102 | } 103 | if exp, got := ch.IsBanned("notnick!user@host"), false; exp != got { 104 | t.Errorf("Expected: %v, got: %v", exp, got) 105 | } 106 | if exp, got := ch.IsBanned("notnick!user@host.com"), true; exp != got { 107 | t.Errorf("Expected: %v, got: %v", exp, got) 108 | } 109 | if exp, got := ch.IsBanned("nick2!user@host"), true; exp != got { 110 | t.Errorf("Expected: %v, got: %v", exp, got) 111 | } 112 | 113 | ch.DeleteBans("") 114 | if exp, got := len(ch.Bans()), 3; exp != got { 115 | t.Errorf("Expected: %v, got: %v", exp, got) 116 | } 117 | 118 | ch.DeleteBans("nick") 119 | if exp, got := ch.IsBanned("nick"), false; exp != got { 120 | t.Errorf("Expected: %v, got: %v", exp, got) 121 | } 122 | if exp, got := ch.IsBanned("notnick"), false; exp != got { 123 | t.Errorf("Expected: %v, got: %v", exp, got) 124 | } 125 | if exp, got := ch.IsBanned("nick!user@host"), false; exp != got { 126 | t.Errorf("Expected: %v, got: %v", exp, got) 127 | } 128 | if exp, got := ch.IsBanned("nick2!user@host"), true; exp != got { 129 | t.Errorf("Expected: %v, got: %v", exp, got) 130 | } 131 | if exp, got := ch.IsBanned("notnick!user@host"), false; exp != got { 132 | t.Errorf("Expected: %v, got: %v", exp, got) 133 | } 134 | if exp, got := ch.IsBanned("notnick!user@host.com"), true; exp != got { 135 | t.Errorf("Expected: %v, got: %v", exp, got) 136 | } 137 | if exp, got := ch.IsBanned("nick2!user@host"), true; exp != got { 138 | t.Errorf("Expected: %v, got: %v", exp, got) 139 | } 140 | 141 | if exp, got := len(ch.Bans()), 2; exp != got { 142 | t.Errorf("Expected: %v, got: %v", exp, got) 143 | } 144 | 145 | ch.DeleteBans("nick2!user@host.com") 146 | if exp, got := ch.IsBanned("nick2!user@host"), false; exp != got { 147 | t.Errorf("Expected: %v, got: %v", exp, got) 148 | } 149 | if exp, got := ch.IsBanned("notnick!user@host.com"), false; exp != got { 150 | t.Errorf("Expected: %v, got: %v", exp, got) 151 | } 152 | if exp, got := ch.IsBanned("nick2!user@host"), false; exp != got { 153 | t.Errorf("Expected: %v, got: %v", exp, got) 154 | } 155 | 156 | if exp, got := len(ch.Bans()), 0; exp != got { 157 | t.Errorf("Expected: %v, got: %v", exp, got) 158 | } 159 | ch.DeleteBans("nick2!user@host.com") 160 | if exp, got := len(ch.Bans()), 0; exp != got { 161 | t.Errorf("Expected: %v, got: %v", exp, got) 162 | } 163 | } 164 | 165 | func TestChannel_String(t *testing.T) { 166 | t.Parallel() 167 | 168 | ch := NewChannel("name", testKinds) 169 | if exp, got := ch.String(), "name"; exp != got { 170 | t.Errorf("Expected: %v, got: %v", exp, got) 171 | } 172 | } 173 | 174 | func TestChannel_JSONify(t *testing.T) { 175 | t.Parallel() 176 | 177 | a := &Channel{ 178 | Name: "a", 179 | Topic: "b", 180 | } 181 | var b Channel 182 | 183 | str, err := json.Marshal(a) 184 | if err != nil { 185 | t.Error(err) 186 | } 187 | 188 | jsonStr := `{"name":"a","topic":"b",` + 189 | `"channel_modes":{"modes":null,"arg_modes":null,` + 190 | `"address_modes":null,"addresses":0,"mode_kinds":null}}` 191 | 192 | if string(str) != jsonStr { 193 | t.Errorf("Wrong JSON: %s", str) 194 | } 195 | 196 | if err = json.Unmarshal(str, &b); err != nil { 197 | t.Error(err) 198 | } 199 | 200 | if !reflect.DeepEqual(*a, b) { 201 | t.Error("A and B differ:", a, b) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ultimateq 2 | 3 | [![Build Status](https://circleci.com/gh/aarondl/ultimateq.svg?style=shield)](https://circleci.com/gh/aarondl/ultimateq) [![Coverage Status](http://coveralls.io/repos/aarondl/ultimateq/badge.png?branch=master)](http://coveralls.io/r/aarondl/ultimateq?branch=master) 4 | 5 | An irc bot framework written in Go. 6 | 7 | ultimateq is a distributed irc bot framework. It allows you to create a bot 8 | with a single file (see simple.go for a good example), or to create many 9 | extensions that can run independently and hook them up to one bot, or allow 10 | many bots to connect to them. 11 | 12 | What follows is a sample of the bot api for some basic greeter bot. 13 | Keep in mind that he can use much more fine-grained APIs allowing you more 14 | control of how he's run. See simple.go for a much bigger example. 15 | 16 | The bot.Run() function reads in a config.toml, sets up keyboard and signal 17 | handlers, and runs the bot until all networks are permanently disconnected. 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "log" 24 | 25 | "github.com/aarondl/ultimateq/bot" 26 | "github.com/aarondl/ultimateq/dispatch/cmd" 27 | "github.com/aarondl/ultimateq/irc" 28 | ) 29 | 30 | // Greeter will be our handler type for both commands and simple events. 31 | type Greeter struct{} 32 | 33 | // Cmd is the interface method that's required by all command handlers, this 34 | // method is fallback for any commands that are not reachable through reflection 35 | func (_ Greeter) Cmd(command string, _ irc.Writer, _ *cmd.Event) error { 36 | switch command { 37 | case "hello": 38 | // Do something 39 | } 40 | return nil 41 | } 42 | 43 | // This method will be invoked via reflection based on the command name. This 44 | // way we don't need the switch case above. If this method did not exist it 45 | // would fallback to the above method when the command was issued by a user. 46 | func (_ Greeter) Hello(w irc.Writer, e *cmd.Event) error { 47 | // Write out our message. 48 | // Normally we'd have to check if e.Channel is nil, but it's safe since 49 | // we registered with the cmd.PUBLIC (no private message allowed) flag for 50 | // msg scope. 51 | w.Privmsgf(e.Channel.Name(), "Hello to you too %s!", e.Nick()) 52 | 53 | return nil 54 | } 55 | 56 | /* 57 | Currently one of these functions is required to handle events, they all do 58 | some basic event filtering, for example PrivmsgChannel will only get PRIVMSG 59 | events that are sent to a channel. 60 | HandleRaw(irc.Writer, *irc.Event) 61 | Privmsg(irc.Writer, *irc.Event) 62 | PrivmsgUser(irc.Writer, *irc.Event) 63 | PrivmsgChannel(irc.Writer, *irc.Event) 64 | Notice(irc.Writer, *irc.Event) 65 | NoticeUser(irc.Writer, *irc.Event) 66 | NoticeChannel(irc.Writer, *irc.Event) 67 | CTCP(irc.Writer, *irc.Event, string, string) 68 | CTCPChannel(irc.Writer, *irc.Event, string, string) 69 | CTCPReply(irc.Writer, *irc.Event, string, string) 70 | */ 71 | func (_ Greeter) HandleRaw(w irc.Writer, e *irc.Event) { 72 | // Because of the way we've registered, only JOIN events will be dispatched, 73 | // but for later growth we could do this. 74 | switch e.Name { 75 | case irc.JOIN: 76 | // Write the message out. 77 | w.Privmsgf(e.Target(), "Welcome to %s %s!", e.Target(), e.Nick()) 78 | } 79 | } 80 | 81 | func main() { 82 | err := bot.Run(func(b *bot.Bot) { 83 | // Basic Command - See cmd package documentation. 84 | b.RegisterCmd(cmd.MkCmd( 85 | "myExtension", 86 | "Says hello to someone", 87 | "hello", 88 | &Greeter{}, 89 | cmd.PRIVMSG, cmd.PUBLIC, 90 | )) 91 | 92 | // To make an Authenticated command using the user database simply 93 | // use the cmd.MkAuthCmd method to create your commands. 94 | // b.RegisterCmd(cmd.MkAuthCmd(... 95 | 96 | // Basic Handler 97 | b.Register(irc.JOIN, &Greeter{}) 98 | }) 99 | 100 | log.Println(err) 101 | } 102 | ``` 103 | 104 | Here's a quick sample config.toml for use with the above, see the config 105 | package documentation for a full configuration sample. 106 | 107 | ```toml 108 | nick = "Bot" 109 | altnick = "Bot" 110 | username = "notabot" 111 | realname = "A real bot" 112 | 113 | [networks.test] 114 | servers = ["localhost:3337"] 115 | ssl = true 116 | ``` 117 | 118 | ## Package status 119 | 120 | The bot is roughly 60% complete. The internal packages are nearly 100% 121 | complete except the outstanding issues that don't involve extensions here: 122 | https://github.com/aarondl/ultimateq/issues/ 123 | 124 | The following major pieces are currently missing: 125 | 126 | * Front ends for people who want an out of the box bot. 127 | * Extensions. 128 | 129 | ## Packages 130 | 131 | #### bot 132 | This package ties all the low level plumbing together, using this package's 133 | helpers it should be easy to create a bot and deploy him into the dying world 134 | of irc. 135 | 136 | #### irc 137 | This package houses the common irc constants and types necessary throughout 138 | the bot. It's supposed to remain a small and dependency-less package that all 139 | packages can utilize. 140 | 141 | #### config 142 | Config package is used to present a fluent style configuration builder for an 143 | irc bot. It also provides some validation, and file reading/writing. 144 | 145 | #### parse 146 | This package deals with parsing irc protocols. It is able to consume irc 147 | protocol messages using the Parse method, returning the common irc.Event 148 | type from the irc package. 149 | 150 | #### dispatch 151 | Dispatch package is meant to register callbacks and dispatch irc.Events onto 152 | them in a new goroutine. It also presents many handler interfaces that help 153 | filter messages. 154 | 155 | #### dispatch/cmd 156 | Cmd package is a much more involved version of the dispatcher. Instead of 157 | simply responding to irc raw messages, the commander parses arguments, handles 158 | user authentication and privelege checks. It can also do advanced argument 159 | handling such as returning a user message's target channels or user arguments 160 | from the command. 161 | 162 | #### inet 163 | Implements the actual connection to an irc server, handles buffering, \r\n 164 | splitting and appending, filtering, and write-speed throttling. 165 | 166 | #### extension 167 | This package defines helpers to create an extension for the bot. It should 168 | expose a way to connect/allow connections to the bot via TCP or Unix socket. 169 | And have simple helpers for some network RPC mechanism. 170 | 171 | #### data 172 | This package holds state information for all the irc servers. It can also store 173 | user and channel data, and authenticate users to be used in protected commands 174 | from the cmd package. The database is a key-value store written in Go. 175 | (Many thanks to Jan Merci for this great package: https://github.com/cznic/kv) 176 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestConfig_New(t *testing.T) { 10 | t.Parallel() 11 | 12 | c := New() 13 | if c == nil { 14 | t.Error("Expected a configuration to be created.") 15 | } 16 | } 17 | 18 | func TestConfig_Clear(t *testing.T) { 19 | t.Parallel() 20 | 21 | c := New() 22 | c.values = map[string]interface{}{"network": "something"} 23 | c.errors = errList{errors.New("something")} 24 | c.filename = "filename" 25 | 26 | c.Clear() 27 | if len(c.values) != 0 { 28 | t.Error("Values should be blank, got:", c.values) 29 | } 30 | if len(c.errors) != 0 { 31 | t.Error("Filename should be blank, got:", c.errors) 32 | } 33 | if len(c.filename) != 0 { 34 | t.Error("Filename should be blank, got:", c.filename) 35 | } 36 | } 37 | 38 | func TestConfig_Replace(t *testing.T) { 39 | t.Parallel() 40 | 41 | c1 := New().FromString(`nick = "hello"`) 42 | c2 := New().FromString(`nick = "there"`) 43 | 44 | if val, ok := c1.Network("").Nick(); !ok || val != "hello" { 45 | t.Error(`Expected nick to be "hello", got:`, val) 46 | } 47 | if val, ok := c1.Replace(c2).Network("").Nick(); !ok || val != "there" { 48 | t.Error(`Expected nick to be "there", got:`, val) 49 | } 50 | } 51 | 52 | func TestConfig_Clone(t *testing.T) { 53 | t.Parallel() 54 | 55 | c := New().FromString(` 56 | string = "str" 57 | [[channels]] 58 | uint = 5 59 | [networks.ircnet] 60 | servers = ["str"] 61 | `) 62 | 63 | c.NewNetwork("othernet"). 64 | SetServers([]string{"str"}). 65 | SetChannels([]Channel{{"a", "b", "c"}}) 66 | 67 | nc := c.Clone() 68 | 69 | checkMap(nc.values, c.values, t) 70 | } 71 | 72 | // checkMap is essentially useless, but hopefully it's doing something. 73 | func checkMap(dest, src mp, t *testing.T) { 74 | for key, value := range src { 75 | switch v := value.(type) { 76 | case map[string]interface{}: 77 | checkMap(dest.get(key), v, t) 78 | case mp: 79 | checkMap(dest.get(key), v, t) 80 | case []Channel: 81 | destChans := dest[key].([]Channel) 82 | for i, c := range v { 83 | if &c == &destChans[i] { 84 | t.Error("Expected channels to be deep copied.") 85 | } 86 | } 87 | // The Following cases are immutable so we don't care so much. 88 | case string: 89 | case int: 90 | case uint: 91 | case float64: 92 | default: 93 | orig := reflect.ValueOf(v) 94 | clone := reflect.ValueOf(dest[key]) 95 | if reflect.DeepEqual(orig, clone) { 96 | t.Errorf("Expected %s to be deep copied.", key) 97 | } 98 | } 99 | } 100 | } 101 | 102 | func TestConfig_Networks(t *testing.T) { 103 | t.Parallel() 104 | 105 | if New().Networks() != nil { 106 | t.Error("Expected networks to be empty.") 107 | } 108 | 109 | c := New().FromString(configuration) 110 | 111 | nets := c.Networks() 112 | exps := []string{"ircnet", "noirc"} 113 | 114 | for _, exp := range exps { 115 | found := false 116 | for _, net := range nets { 117 | if net == exp { 118 | found = true 119 | break 120 | } 121 | } 122 | if !found { 123 | t.Errorf("Did not find: %s in network list.", exp) 124 | } 125 | } 126 | } 127 | 128 | func TestConfig_Exts(t *testing.T) { 129 | t.Parallel() 130 | 131 | if New().Exts() != nil { 132 | t.Error("Expected exts to be empty.") 133 | } 134 | 135 | c := New().FromString(configuration) 136 | exts := c.Exts() 137 | exps := []string{"myext"} 138 | 139 | for _, exp := range exps { 140 | found := false 141 | for _, ext := range exts { 142 | if ext == exp { 143 | found = true 144 | break 145 | } 146 | } 147 | if !found { 148 | t.Errorf("Did not find: %s in ext list.", exp) 149 | } 150 | } 151 | } 152 | 153 | func TestConfig_Contexts(t *testing.T) { 154 | t.Parallel() 155 | 156 | c := New().FromString(configuration) 157 | 158 | if c.Network("") == nil { 159 | t.Error("Expected to be able to get global context.") 160 | } 161 | if c.Network("ircnet") == nil { 162 | t.Error("Expected to be able to get network context.") 163 | } 164 | if c.ExtGlobal() == nil { 165 | t.Error("Expected to be able to get ext context.") 166 | } 167 | if c.Ext("myext") == nil { 168 | t.Error("Expected to be able to get ext context.") 169 | } 170 | 171 | if ctx := c.Network("nonexistent"); ctx != nil { 172 | t.Error("Should retrieve no context for non-existent things, got:", ctx) 173 | } 174 | if ctx := c.Ext("nonexistent"); ctx != nil { 175 | t.Error("Should retrieve no context for non-existent things, got:", ctx) 176 | } 177 | } 178 | 179 | func TestConfig_NewThings(t *testing.T) { 180 | t.Parallel() 181 | 182 | c := New() 183 | if net := c.NewNetwork("net1"); net == nil { 184 | t.Error("Should have created a new network.") 185 | } 186 | if ext := c.NewExt("ext1"); ext == nil { 187 | t.Error("Should have created a new extension.") 188 | } 189 | 190 | if net := c.NewNetwork("net2"); net == nil { 191 | t.Error("Should have created a new network.") 192 | } 193 | if ext := c.NewExt("ext2"); ext == nil { 194 | t.Error("Should have created a new extension.") 195 | } 196 | 197 | if net := c.NewNetwork("net1"); net != nil { 198 | t.Error("Should not have created a new network.") 199 | } 200 | if ext := c.NewExt("ext2"); ext != nil { 201 | t.Error("Should not have created a new extension.") 202 | } 203 | } 204 | 205 | func TestConfig_Config_GetSet(t *testing.T) { 206 | t.Parallel() 207 | 208 | c := New() 209 | if v, ok := c.StoreFile(); ok || v != defaultStoreFile { 210 | t.Error("Expected store file not to be set, and to get default:", v) 211 | } 212 | c.SetStoreFile("a") 213 | if v, ok := c.StoreFile(); !ok || v != "a" { 214 | t.Error("Expected store file to be set, and to get a, got:", v) 215 | } 216 | 217 | if v, ok := c.LogFile(); ok || v != "" { 218 | t.Error("Expected log file not to be set, and to get default:", v) 219 | } 220 | c.SetLogFile("a") 221 | if v, ok := c.LogFile(); !ok || v != "a" { 222 | t.Error("Expected log file to be set, and to get a, got:", v) 223 | } 224 | 225 | if v, ok := c.LogLevel(); ok || v != defaultLogLevel { 226 | t.Error("Expected log level not to be set, and to get default:", v) 227 | } 228 | c.SetLogLevel("a") 229 | if v, ok := c.LogLevel(); !ok || v != "a" { 230 | t.Error("Expected log level to be set, and to get a, got:", v) 231 | } 232 | 233 | if v, ok := c.NoCoreCmds(); ok || v != false { 234 | t.Error("Expected no core cmds not to be set, and to get default:", v) 235 | } 236 | c.SetNoCoreCmds(true) 237 | if v, ok := c.NoCoreCmds(); !ok || v != true { 238 | t.Error("Expected no core cmds to be set, and to get a, got:", v) 239 | } 240 | 241 | if v, ok := c.SecretKey(); ok || v != "" { 242 | t.Error("Expected secret key not to be set, and to get default:", v) 243 | } 244 | c.SetSecretKey("a") 245 | if v, ok := c.SecretKey(); !ok || v != "a" { 246 | t.Error("Expected secret key to be set, and to get a, got:", v) 247 | } 248 | if v, ok := c.Ignores(); ok || v != nil { 249 | t.Error("Expected ignores not to be set, and to get default:", v) 250 | } 251 | c.SetIgnores([]string{"a", "b"}) 252 | if v, ok := c.Ignores(); !ok || v[0] != "a" || v[1] != "b" { 253 | t.Error("Expected ignores to be set, and to get a, got:", v) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /config/network_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestConfig_Network_GetSet(t *testing.T) { 9 | t.Parallel() 10 | 11 | c := New() 12 | glb := c.Network("") 13 | net := c.NewNetwork("net") 14 | 15 | check("Nick", "", "nick1", "nick2", glb, net, t) 16 | 17 | check("Altnick", "", "altnick1", "altnick2", glb, net, t) 18 | 19 | check("Username", "", "username1", "username2", glb, net, t) 20 | 21 | check("Realname", "", "realname1", "realname2", glb, net, t) 22 | 23 | check("Password", "", "password1", "password2", glb, net, t) 24 | 25 | check("TLS", false, false, true, glb, net, t) 26 | 27 | check("TLSCACert", "", "tlscert1", "tlscert2", glb, net, t) 28 | 29 | check("TLSCert", "", "tlscert1", "tlscert2", glb, net, t) 30 | 31 | check("TLSKey", "", "tlscert1", "tlscert2", glb, net, t) 32 | 33 | check("TLSInsecureSkipVerify", false, false, true, glb, net, t) 34 | 35 | check("NoState", false, false, true, glb, net, t) 36 | 37 | check("NoStore", false, false, true, glb, net, t) 38 | 39 | check("NoAutoJoin", false, false, true, glb, net, t) 40 | 41 | check("JoinDelay", defaultJoinDelay, uint(20), uint(30), 42 | glb, net, t) 43 | 44 | check("FloodLenPenalty", defaultFloodLenPenalty, uint(20), uint(30), 45 | glb, net, t) 46 | 47 | check("FloodTimeout", defaultFloodTimeout, 20.0, 30.0, glb, net, t) 48 | 49 | check("FloodStep", defaultFloodStep, 20.0, 30.0, glb, net, t) 50 | 51 | check("KeepAlive", defaultKeepAlive, 20.0, 30.0, glb, net, t) 52 | 53 | check("NoReconnect", false, false, true, glb, net, t) 54 | 55 | check("ReconnectTimeout", defaultReconnectTimeout, 56 | uint(20), uint(30), glb, net, t) 57 | 58 | check("Prefix", '.', '!', '@', glb, net, t) 59 | 60 | if srvs, ok := net.Servers(); ok || len(srvs) != 0 { 61 | t.Error("Expected servers to be empty.") 62 | } 63 | 64 | net.SetServers([]string{"srv"}) 65 | 66 | if srvs, ok := net.Servers(); !ok || len(srvs) != 1 { 67 | t.Error("Expected servers not to be empty.") 68 | } else if srvs[0] != "srv" { 69 | t.Error("Expected the first server to be srv, got:", srvs[0]) 70 | } 71 | } 72 | 73 | func TestConfig_Network_GetSetChannels(t *testing.T) { 74 | t.Parallel() 75 | 76 | c := New() 77 | glb := c.Network("") 78 | net := c.NewNetwork("net") 79 | ch1 := Channel{"a", "b", "c"} 80 | ch2 := Channel{"a", "b", "c"} 81 | 82 | if chans, ok := glb.Channels(); ok || len(chans) != 0 { 83 | t.Error("Expected servers to be empty.") 84 | } 85 | if chans, ok := net.Channels(); ok || len(chans) != 0 { 86 | t.Error("Expected servers to be empty.") 87 | } 88 | 89 | glb.SetChannels([]Channel{ch1}) 90 | 91 | if chans, ok := glb.Channels(); !ok || len(chans) != 1 { 92 | t.Error("Expected servers not to be empty.") 93 | } else if chans["a"] != ch1 { 94 | t.Errorf("Expected the first channel to be %v, got: %v", ch1, chans["a"]) 95 | } 96 | if chans, ok := net.Channels(); !ok || len(chans) != 1 { 97 | t.Error("Expected servers not to be empty.") 98 | } else if chans["a"] != ch1 { 99 | t.Errorf("Expected the first channel to be %v, got: %v", ch1, chans["a"]) 100 | } 101 | 102 | net.SetChannels([]Channel{ch2}) 103 | 104 | if chans, ok := glb.Channels(); !ok || len(chans) != 1 { 105 | t.Error("Expected servers not to be empty.") 106 | } else if chans["a"] != ch1 { 107 | t.Errorf("Expected the first channel to be %v, got: %v", ch1, chans["a"]) 108 | } 109 | if chans, ok := net.Channels(); !ok || len(chans) != 1 { 110 | t.Error("Expected servers not to be empty.") 111 | } else if chans["a"] != ch2 { 112 | t.Errorf("Expected the first channel to be %v, got: %v", ch2, chans["a"]) 113 | } 114 | 115 | // Test Coverage, retrieve a value that's not possible. 116 | c.values["channels"] = 5 117 | if chans, ok := glb.Channels(); ok || len(chans) != 0 { 118 | t.Error("Expected servers to be empty.") 119 | } 120 | } 121 | 122 | func TestConfig_Network_GetChannelPrefix(t *testing.T) { 123 | t.Parallel() 124 | 125 | c := New() 126 | glb := c.Network("") 127 | net := c.NewNetwork("net") 128 | ch1 := Channel{"a", "aa", "1"} 129 | ch2 := Channel{"b", "bb", "1"} 130 | 131 | if pfx, ok := glb.ChannelPrefix("a"); ok || pfx != defaultPrefix { 132 | t.Error("Expected the prefix to not be set.") 133 | } 134 | if pfx, ok := net.ChannelPrefix("b"); ok || pfx != defaultPrefix { 135 | t.Error("Expected the prefix to not be set.") 136 | } 137 | 138 | glb.SetChannels([]Channel{ch1, ch2}) 139 | if pfx, ok := glb.ChannelPrefix("a"); !ok || pfx != '1' { 140 | t.Error("Expected the prefix be set.") 141 | } 142 | if pfx, ok := net.ChannelPrefix("a"); !ok || pfx != '1' { 143 | t.Error("Expected the prefix be set.") 144 | } 145 | 146 | ch1.Prefix = "2" 147 | net.SetChannels([]Channel{ch1, ch2}) 148 | if pfx, ok := glb.ChannelPrefix("a"); !ok || pfx != '1' { 149 | t.Error("Expected the prefix be set.") 150 | } 151 | if pfx, ok := net.ChannelPrefix("a"); !ok || pfx != '2' { 152 | t.Error("Expected the prefix be set.") 153 | } 154 | 155 | if pfx, ok := net.ChannelPrefix("nochan"); ok || pfx != defaultPrefix { 156 | t.Error("Expected the prefix not be set.") 157 | } 158 | } 159 | 160 | func check( 161 | name string, defaultVal, afterGlobal, afterNetwork interface{}, 162 | global, network *NetCTX, t *testing.T) { 163 | 164 | ctxType := reflect.TypeOf(network) 165 | def := reflect.ValueOf(defaultVal) 166 | aGlobal := reflect.ValueOf(afterGlobal) 167 | aNetwork := reflect.ValueOf(afterNetwork) 168 | glb := reflect.ValueOf(global) 169 | net := reflect.ValueOf(network) 170 | 171 | get, ok := ctxType.MethodByName(name) 172 | set, ok := ctxType.MethodByName("Set" + name) 173 | 174 | var exp, got interface{} 175 | var ret []reflect.Value 176 | getargs := make([]reflect.Value, 1) 177 | setargs := make([]reflect.Value, 2) 178 | 179 | getargs[0] = glb 180 | ret = get.Func.Call(getargs) 181 | exp, got, ok = def.Interface(), ret[0].Interface(), ret[1].Bool() 182 | if !reflect.DeepEqual(exp, got) || ok { 183 | t.Errorf("Expected %s to be: %#v, got: %#v", name, exp, got) 184 | } 185 | getargs[0] = net 186 | ret = get.Func.Call(getargs) 187 | exp, got, ok = def.Interface(), ret[0].Interface(), ret[1].Bool() 188 | if !reflect.DeepEqual(exp, got) || ok { 189 | t.Errorf("Expected %s to be: %#v, got: %#v", name, exp, got) 190 | } 191 | 192 | setargs[0], setargs[1] = glb, aGlobal 193 | set.Func.Call(setargs) 194 | 195 | getargs[0] = glb 196 | ret = get.Func.Call(getargs) 197 | exp, got, ok = aGlobal.Interface(), ret[0].Interface(), ret[1].Bool() 198 | if !reflect.DeepEqual(exp, got) || !ok { 199 | t.Errorf("Expected %s to be: %#v, got: %#v", name, exp, got) 200 | } 201 | getargs[0] = net 202 | ret = get.Func.Call(getargs) 203 | exp, got, ok = aGlobal.Interface(), ret[0].Interface(), ret[1].Bool() 204 | if !reflect.DeepEqual(exp, got) || !ok { 205 | t.Errorf("Expected %s to be: %#v, got: %#v", name, exp, got) 206 | } 207 | 208 | setargs[0], setargs[1] = net, aNetwork 209 | set.Func.Call(setargs) 210 | 211 | getargs[0] = glb 212 | ret = get.Func.Call(getargs) 213 | exp, got, ok = aGlobal.Interface(), ret[0].Interface(), ret[1].Bool() 214 | if !reflect.DeepEqual(exp, got) || !ok { 215 | t.Errorf("Expected %s to be: %#v, got: %#v", name, exp, got) 216 | } 217 | getargs[0] = net 218 | ret = get.Func.Call(getargs) 219 | exp, got, ok = aNetwork.Interface(), ret[0].Interface(), ret[1].Bool() 220 | if !reflect.DeepEqual(exp, got) || !ok { 221 | t.Errorf("Expected %s to be: %#v, got: %#v", name, exp, got) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /data/modes_common_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var testUserKindStr = `(ov)@+` 10 | var testChannelKindStr = `b,c,d,axyz` 11 | var testKinds, _ = newModeKinds(testUserKindStr, testChannelKindStr) 12 | 13 | func TestModeKinds_Create(t *testing.T) { 14 | t.Parallel() 15 | 16 | m, err := newModeKinds(testUserKindStr, "a,b,c,d") 17 | if err != nil { 18 | t.Error("Unexpected error:", err) 19 | } 20 | if got, exp := m.channelModes['a'], ARGS_ADDRESS; exp != got { 21 | t.Errorf("Expected: %v, got: %v", exp, got) 22 | } 23 | if got, exp := m.channelModes['b'], ARGS_ALWAYS; exp != got { 24 | t.Errorf("Expected: %v, got: %v", exp, got) 25 | } 26 | if got, exp := m.channelModes['c'], ARGS_ONSET; exp != got { 27 | t.Errorf("Expected: %v, got: %v", exp, got) 28 | } 29 | if got, exp := m.channelModes['d'], ARGS_NONE; exp != got { 30 | t.Errorf("Expected: %v, got: %v", exp, got) 31 | } 32 | 33 | m, err = newModeKinds("(o)@", "a, b, c, d") 34 | if err != nil { 35 | t.Error("Unexpected error:", err) 36 | } 37 | if got, exp := m.channelModes['a'], ARGS_ADDRESS; exp != got { 38 | t.Errorf("Expected: %v, got: %v", exp, got) 39 | } 40 | if got, exp := m.channelModes['b'], ARGS_ALWAYS; exp != got { 41 | t.Errorf("Expected: %v, got: %v", exp, got) 42 | } 43 | if got, exp := m.channelModes['c'], ARGS_ONSET; exp != got { 44 | t.Errorf("Expected: %v, got: %v", exp, got) 45 | } 46 | if got, exp := m.channelModes['d'], ARGS_NONE; exp != got { 47 | t.Errorf("Expected: %v, got: %v", exp, got) 48 | } 49 | 50 | err = m.update("(o)@", "d, c, b, a") 51 | if err != nil { 52 | t.Error("Unexpected error:", err) 53 | } 54 | if got, exp := m.channelModes['d'], ARGS_ADDRESS; exp != got { 55 | t.Errorf("Expected: %v, got: %v", exp, got) 56 | } 57 | if got, exp := m.channelModes['c'], ARGS_ALWAYS; exp != got { 58 | t.Errorf("Expected: %v, got: %v", exp, got) 59 | } 60 | if got, exp := m.channelModes['b'], ARGS_ONSET; exp != got { 61 | t.Errorf("Expected: %v, got: %v", exp, got) 62 | } 63 | if got, exp := m.channelModes['a'], ARGS_NONE; exp != got { 64 | t.Errorf("Expected: %v, got: %v", exp, got) 65 | } 66 | } 67 | 68 | func TestModeKindsUpdate(t *testing.T) { 69 | t.Parallel() 70 | 71 | m, err := newModeKinds(testUserKindStr, "a,b,c,d") 72 | if got, exp := m.channelModes['a'], ARGS_ADDRESS; exp != got { 73 | t.Errorf("Expected: %v, got: %v", exp, got) 74 | } 75 | if got, exp := m.channelModes['b'], ARGS_ALWAYS; exp != got { 76 | t.Errorf("Expected: %v, got: %v", exp, got) 77 | } 78 | if got, exp := m.channelModes['c'], ARGS_ONSET; exp != got { 79 | t.Errorf("Expected: %v, got: %v", exp, got) 80 | } 81 | if got, exp := m.channelModes['d'], ARGS_NONE; exp != got { 82 | t.Errorf("Expected: %v, got: %v", exp, got) 83 | } 84 | 85 | err = m.update(testUserKindStr, "d,c,b,a") 86 | if err != nil { 87 | t.Error("Unexpected Errorf:", err) 88 | } 89 | if got, exp := m.channelModes['d'], ARGS_ADDRESS; exp != got { 90 | t.Errorf("Expected: %v, got: %v", exp, got) 91 | } 92 | if got, exp := m.channelModes['c'], ARGS_ALWAYS; exp != got { 93 | t.Errorf("Expected: %v, got: %v", exp, got) 94 | } 95 | if got, exp := m.channelModes['b'], ARGS_ONSET; exp != got { 96 | t.Errorf("Expected: %v, got: %v", exp, got) 97 | } 98 | if got, exp := m.channelModes['a'], ARGS_NONE; exp != got { 99 | t.Errorf("Expected: %v, got: %v", exp, got) 100 | } 101 | } 102 | 103 | func TestUserModeKinds_Create(t *testing.T) { 104 | t.Parallel() 105 | 106 | u, err := newModeKinds("", testChannelKindStr) 107 | if got := u; got != nil { 108 | t.Errorf("Expected: %v to be nil.", got) 109 | } 110 | if err == nil { 111 | t.Errorf("Unexpected nil.") 112 | } 113 | u, err = newModeKinds("a", testChannelKindStr) 114 | if got := u; got != nil { 115 | t.Errorf("Expected: %v to be nil.", got) 116 | } 117 | if err == nil { 118 | t.Errorf("Unexpected nil.") 119 | } 120 | u, err = newModeKinds("(a", testChannelKindStr) 121 | if got := u; got != nil { 122 | t.Errorf("Expected: %v to be nil.", got) 123 | } 124 | if err == nil { 125 | t.Errorf("Unexpected nil.") 126 | } 127 | 128 | u, err = newModeKinds("(abcdefghi)!@#$%^&*_", testChannelKindStr) 129 | if got := u; got != nil { 130 | t.Errorf("Expected: %v to be nil.", got) 131 | } 132 | if err == nil { 133 | t.Errorf("Unexpected nil.") 134 | } 135 | 136 | u, err = newModeKinds("(ov)@+", testChannelKindStr) 137 | if u == nil { 138 | t.Errorf("Unexpected nil.") 139 | } 140 | if err != nil { 141 | t.Error("Unexpected Error:", err) 142 | } 143 | if got, exp := u.userPrefixes[0], [2]rune{'o', '@'}; exp != got { 144 | t.Errorf("Expected: %v, got: %v", exp, got) 145 | } 146 | if got, exp := u.userPrefixes[1], [2]rune{'v', '+'}; exp != got { 147 | t.Errorf("Expected: %v, got: %v", exp, got) 148 | } 149 | } 150 | 151 | func TestUserModeKinds_Symbol(t *testing.T) { 152 | t.Parallel() 153 | 154 | u, err := newModeKinds("(ov)@+", testChannelKindStr) 155 | if err != nil { 156 | t.Error("Unexpected Error:", err) 157 | } 158 | if got, exp := u.Symbol('o'), '@'; exp != got { 159 | t.Errorf("Expected: %v, got: %v", exp, got) 160 | } 161 | if got, exp := u.Symbol(' '), rune(0); exp != got { 162 | t.Errorf("Expected: %v, got: %v", exp, got) 163 | } 164 | } 165 | 166 | func TestUserModeKinds_Mode(t *testing.T) { 167 | t.Parallel() 168 | 169 | u, err := newModeKinds("(ov)@+", testChannelKindStr) 170 | if err != nil { 171 | t.Error("Unexpected Error:", err) 172 | } 173 | if got, exp := u.Mode('@'), 'o'; exp != got { 174 | t.Errorf("Expected: %v, got: %v", exp, got) 175 | } 176 | if got, exp := u.Mode(' '), rune(0); exp != got { 177 | t.Errorf("Expected: %v, got: %v", exp, got) 178 | } 179 | } 180 | 181 | func TestUserModeKinds_Update(t *testing.T) { 182 | t.Parallel() 183 | 184 | u, err := newModeKinds("(ov)@+", testChannelKindStr) 185 | if err != nil { 186 | t.Error("Unexpected Error:", err) 187 | } 188 | if got, exp := u.modeBit('o'), byte(0); exp == got { 189 | t.Errorf("Did not want: %v, got: %v", exp, got) 190 | } 191 | err = u.update("(v)+", "") 192 | if err != nil { 193 | t.Error("Unexpected Error:", err) 194 | } 195 | if got, exp := u.modeBit('o'), byte(0); exp != got { 196 | t.Errorf("Expected: %v, got: %v", exp, got) 197 | } 198 | } 199 | 200 | func TestUserModeKinds_JSONify(t *testing.T) { 201 | t.Parallel() 202 | 203 | a := testKinds 204 | var b modeKinds 205 | 206 | str, err := json.Marshal(a) 207 | if err != nil { 208 | t.Error(err) 209 | } 210 | 211 | jsonStr := `{"user_prefixes":[["o","@"],["v","+"]],` + 212 | `"channel_modes":{"a":1,"b":4,"c":2,"d":3,"x":1,"y":1,"z":1}}` 213 | 214 | if string(str) != jsonStr { 215 | t.Errorf("Wrong JSON: %s", str) 216 | } 217 | 218 | if err = json.Unmarshal(str, &b); err != nil { 219 | t.Error(err) 220 | } 221 | 222 | if !reflect.DeepEqual(a.userPrefixes, b.userPrefixes) { 223 | t.Error("A and B differ:", a.userPrefixes, b.userPrefixes) 224 | } 225 | if !reflect.DeepEqual(a.channelModes, b.channelModes) { 226 | t.Error("A and B differ:", a.channelModes, b.channelModes) 227 | } 228 | } 229 | 230 | func TestModeKinds_Protofy(t *testing.T) { 231 | t.Parallel() 232 | 233 | a, err := newModeKinds(testUserKindStr, testChannelKindStr) 234 | if err != nil { 235 | t.Error(err) 236 | } 237 | 238 | var b modeKinds 239 | 240 | err = b.FromProto(a.ToProto()) 241 | if err != nil { 242 | t.Error(err) 243 | } 244 | 245 | if !reflect.DeepEqual(a.userPrefixes, b.userPrefixes) { 246 | t.Error("A and B differ:", a.userPrefixes, b.userPrefixes) 247 | } 248 | if !reflect.DeepEqual(a.channelModes, b.channelModes) { 249 | t.Error("A and B differ:", a.channelModes, b.channelModes) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /bot/core_handler_test.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/aarondl/ultimateq/config" 11 | "github.com/aarondl/ultimateq/irc" 12 | ) 13 | 14 | //=================================================================== 15 | // Fixtures for basic responses as well as full bot required messages 16 | //=================================================================== 17 | type testPoint struct { 18 | irc.Helper 19 | buf *bytes.Buffer 20 | srv *Server 21 | } 22 | 23 | func makeTestPoint(srv *Server) *testPoint { 24 | buf := &bytes.Buffer{} 25 | t := &testPoint{irc.Helper{Writer: buf}, buf, srv} 26 | return t 27 | } 28 | 29 | func (t *testPoint) gets() string { 30 | return t.buf.String() 31 | } 32 | 33 | func (t *testPoint) resetTestWritten() { 34 | t.buf.Reset() 35 | } 36 | 37 | func (t *testPoint) GetKey() string { 38 | return netID 39 | } 40 | 41 | //============== 42 | // Tests 43 | //============== 44 | func TestCoreHandler_Ping(t *testing.T) { 45 | handler := coreHandler{} 46 | ev := irc.NewEvent(netID, netInfo, irc.PING, "", "123123123123") 47 | endpoint := makeTestPoint(nil) 48 | handler.Handle(endpoint, ev) 49 | expect := irc.PONG + " :" + ev.Args[0] 50 | if got := endpoint.gets(); got != expect { 51 | t.Errorf("Expected: %s, got: %s", expect, got) 52 | } 53 | } 54 | 55 | func TestCoreHandler_Connect(t *testing.T) { 56 | cnf := fakeConfig.Clone() 57 | net := cnf.Network(netID).SetPassword("password") 58 | 59 | ch1Name, ch2Name := "#channel1", "#channel2" 60 | ch1 := config.Channel{Name: ch1Name, Password: "pass"} 61 | ch2 := config.Channel{Name: ch2Name} 62 | net.SetChannels([]config.Channel{ch1, ch2}) 63 | 64 | b, _ := createBot(cnf, nil, nil, devNull, false, false) 65 | 66 | password, _ := net.Password() 67 | nick, _ := net.Nick() 68 | username, _ := net.Username() 69 | realname, _ := net.Realname() 70 | 71 | handler := coreHandler{bot: b} 72 | msg1 := fmt.Sprintf("PASS :%v", password) 73 | msg2 := fmt.Sprintf("NICK :%v", nick) 74 | msg3 := fmt.Sprintf("USER %v 0 * :%v", username, realname) 75 | msg4 := fmt.Sprintf("JOIN %v %v", ch1Name, ch1.Password) 76 | msg5 := fmt.Sprintf("JOIN %v", ch2Name) 77 | 78 | ev := irc.NewEvent(netID, netInfo, irc.CONNECT, "") 79 | endpoint := makeTestPoint(b.servers[netID]) 80 | handler.Handle(endpoint, ev) 81 | 82 | expect := msg1 + msg2 + msg3 83 | if got := endpoint.gets(); !strings.HasPrefix(got, expect) { 84 | t.Errorf("Expected: %s, got: %s", expect, got) 85 | } else if !strings.Contains(got, msg4) { 86 | t.Errorf("It should try to autojoin #channel1, got: %s", got) 87 | } else if !strings.Contains(got, msg5) { 88 | t.Errorf("It should try to autojoin #channel2, got: %s", got) 89 | } 90 | 91 | endpoint.resetTestWritten() 92 | 93 | net.SetNoAutoJoin(true) 94 | handler.Handle(endpoint, ev) 95 | expect = msg1 + msg2 + msg3 96 | if got := endpoint.gets(); got != expect { 97 | t.Errorf("Expected: %s, got: %s", expect, got) 98 | } 99 | } 100 | 101 | func TestCoreHandler_Nick(t *testing.T) { 102 | b, _ := createBot(fakeConfig, nil, nil, devNull, false, false) 103 | cnf := fakeConfig.Network(netID) 104 | handler := coreHandler{bot: b} 105 | ev := irc.NewEvent(netID, netInfo, irc.ERR_NICKNAMEINUSE, "") 106 | 107 | endpoint := makeTestPoint(b.servers[netID]) 108 | 109 | nick, _ := cnf.Nick() 110 | altnick, _ := cnf.Altnick() 111 | nickstr := "NICK :" 112 | nick1 := nickstr + altnick 113 | nick2 := nickstr + nick + "_" 114 | nick3 := nickstr + nick + "__" 115 | 116 | handler.Handle(endpoint, ev) 117 | if got := endpoint.gets(); got != nick1 { 118 | t.Errorf("Expected: %s, got: %s", nick1, got) 119 | } 120 | endpoint.resetTestWritten() 121 | handler.Handle(endpoint, ev) 122 | if got := endpoint.gets(); got != nick2 { 123 | t.Errorf("Expected: %s, got: %s", nick2, got) 124 | } 125 | endpoint.resetTestWritten() 126 | handler.Handle(endpoint, ev) 127 | if got := endpoint.gets(); got != nick3 { 128 | t.Errorf("Expected: %s, got: %s", nick3, got) 129 | } 130 | } 131 | 132 | func TestCoreHandler_Rejoin(t *testing.T) { 133 | cnf := fakeConfig.Clone() 134 | net := cnf.Network(netID).SetPassword("password").SetNoState(false). 135 | SetNoAutoJoin(true) 136 | 137 | nick, _ := net.Nick() 138 | ch1Name, ch2Name := "#channel1", "#channel2" 139 | ch1 := config.Channel{Name: ch1Name, Password: "pass"} 140 | ch2 := config.Channel{Name: ch2Name} 141 | 142 | b, _ := createBot(cnf, nil, nil, devNull, false, false) 143 | st := b.servers[netID].state 144 | st.Update( 145 | irc.NewEvent(netID, netInfo, irc.RPL_WELCOME, "", "stuff", nick+"!a@b"), 146 | ) 147 | 148 | endpoint := makeTestPoint(b.servers[netID]) 149 | banned := irc.NewEvent(netID, netInfo, irc.ERR_BANNEDFROMCHAN, netID, 150 | nick, ch1Name, "Banned message") 151 | kicked := irc.NewEvent(netID, netInfo, irc.KICK, "badguy", 152 | ch2Name, nick, "Kick Message") 153 | 154 | handler := coreHandler{bot: b} 155 | handler.Handle(endpoint, banned) 156 | handler.Handle(endpoint, kicked) 157 | 158 | if got := endpoint.gets(); len(got) > 0 { 159 | t.Error("Expected nothing to happen with noautojoin set.") 160 | } 161 | 162 | handler.Handle(endpoint, banned) 163 | handler.Handle(endpoint, kicked) 164 | 165 | net.SetNoAutoJoin(false) 166 | 167 | if got := endpoint.gets(); len(got) > 0 { 168 | t.Error("Expected nothing to happen without channels set.") 169 | } 170 | 171 | net.SetChannels([]config.Channel{ch1, ch2}) 172 | 173 | handler.Handle(endpoint, banned) 174 | handler.Handle(endpoint, kicked) 175 | 176 | exp1 := fmt.Sprintf("JOIN %v %v", ch1Name, ch1.Password) 177 | exp2 := fmt.Sprintf("JOIN %v", ch2Name) 178 | got := endpoint.gets() 179 | if !strings.Contains(got, exp1) { 180 | t.Error("Expected it to have joined #channel1 after ban.") 181 | } 182 | if !strings.Contains(got, exp2) { 183 | t.Error("Expected it to have joined #channel1 after kick.") 184 | } 185 | } 186 | 187 | func TestCoreHandler_NetInfo(t *testing.T) { 188 | connProvider := func(srv string) (net.Conn, error) { 189 | return nil, nil 190 | } 191 | 192 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, true, false) 193 | 194 | msg1 := irc.NewEvent(netID, netInfo, irc.RPL_MYINFO, "", 195 | "NICK", "irc.test.net", "testircd-1.2", "acCior", "beiIklmno") 196 | msg2 := irc.NewEvent(netID, netInfo, irc.RPL_ISUPPORT, "", 197 | "RFC8213", "CHANTYPES=&$") 198 | srv := b.servers[netID] 199 | srv.handler.Handle(&testPoint{}, msg1) 200 | srv.handler.Handle(&testPoint{}, msg2) 201 | if got, exp := srv.netInfo.ServerName(), "irc.test.net"; got != exp { 202 | t.Errorf("Expected: %s, got: %s", exp, got) 203 | } 204 | if got, exp := srv.netInfo.IrcdVersion(), "testircd-1.2"; got != exp { 205 | t.Errorf("Expected: %s, got: %s", exp, got) 206 | } 207 | if got, exp := srv.netInfo.Usermodes(), "acCior"; got != exp { 208 | t.Errorf("Expected: %s, got: %s", exp, got) 209 | } 210 | if got, exp := srv.netInfo.LegacyChanmodes(), "beiIklmno"; got != exp { 211 | t.Errorf("Expected: %s, got: %s", exp, got) 212 | } 213 | if got, exp := srv.netInfo.Chantypes(), "&$"; got != exp { 214 | t.Errorf("Expected: %s, got: %s", exp, got) 215 | } 216 | } 217 | 218 | func TestCoreHandler_Join(t *testing.T) { 219 | connProvider := func(srv string) (net.Conn, error) { 220 | return nil, nil 221 | } 222 | 223 | b, _ := createBot(fakeConfig, connProvider, nil, devNull, true, false) 224 | srv := b.servers[netID] 225 | 226 | ev := irc.NewEvent(netID, netInfo, irc.RPL_WELCOME, "server", 227 | "WELCOME", "nick!user@host") 228 | srv.state.Update(ev) 229 | 230 | ev = irc.NewEvent(netID, netInfo, irc.JOIN, 231 | "nick!user@host", "#chan") 232 | 233 | endpoint := makeTestPoint(nil) 234 | srv.handler.Handle(endpoint, ev) 235 | if got, exp := endpoint.gets(), "WHO :#chanMODE :#chan"; got != exp { 236 | t.Errorf("Expected: %s, got: %s", exp, got) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /dispatch/remote/client.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "io/ioutil" 8 | "sync" 9 | "time" 10 | 11 | "google.golang.org/grpc/credentials" 12 | 13 | "github.com/aarondl/ultimateq/dispatch" 14 | "github.com/aarondl/ultimateq/dispatch/cmd" 15 | "github.com/aarondl/ultimateq/irc" 16 | "github.com/pkg/errors" 17 | "google.golang.org/grpc" 18 | 19 | "github.com/aarondl/ultimateq/api" 20 | ) 21 | 22 | // Dial creates a mutually authenticated grpc connection that's given 23 | // directly to NewClient. 24 | // 25 | // If tlsCert and tlsKey are present they will be used as the client's 26 | // certificates and tls is turned on. If tlsCACert is present it will use that 27 | // certificate instead of the system wide CA certificates. And finally if 28 | // insecureSkipVerify is on it will not verify the server's certificate. 29 | func Dial(addr, tlsCert, tlsKey, tlsCACert string, insecureSkipVerify bool) (api.ExtClient, error) { 30 | var opts []grpc.DialOption 31 | if len(tlsCert) != 0 && len(tlsKey) != 0 { 32 | tlsConfig := new(tls.Config) 33 | 34 | clientCert, err := tls.LoadX509KeyPair(tlsCert, tlsKey) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "failed to load client certs") 37 | } 38 | 39 | tlsConfig.Certificates = append(tlsConfig.Certificates, clientCert) 40 | 41 | if len(tlsCACert) != 0 { 42 | caCertBytes, err := ioutil.ReadFile(tlsCACert) 43 | if err != nil { 44 | return nil, errors.Wrap(err, "failed to read ca cert") 45 | } 46 | 47 | certPool := x509.NewCertPool() 48 | certPool.AppendCertsFromPEM(caCertBytes) 49 | tlsConfig.RootCAs = certPool 50 | } 51 | 52 | if insecureSkipVerify { 53 | tlsConfig.InsecureSkipVerify = true 54 | } 55 | 56 | creds := credentials.NewTLS(tlsConfig) 57 | opts = append(opts, grpc.WithTransportCredentials(creds)) 58 | } 59 | 60 | conn, err := grpc.Dial(addr, opts...) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | grpcClient := api.NewExtClient(conn) 66 | return grpcClient, nil 67 | } 68 | 69 | // Client helps handle event and command dispatching remotely. 70 | type Client struct { 71 | client api.ExtClient 72 | extension string 73 | 74 | mut sync.RWMutex 75 | events map[uint64]dispatch.Handler 76 | commands map[uint64]cmd.Handler 77 | } 78 | 79 | // NewClient returns a new dispatcher for extensions. 80 | func NewClient(extension string, client api.ExtClient) *Client { 81 | r := &Client{ 82 | extension: extension, 83 | client: client, 84 | events: make(map[uint64]dispatch.Handler), 85 | commands: make(map[uint64]cmd.Handler), 86 | } 87 | 88 | return r 89 | } 90 | 91 | type remoteIRCWriter struct { 92 | client api.ExtClient 93 | extID string 94 | netID string 95 | } 96 | 97 | func (r remoteIRCWriter) Write(b []byte) (n int, err error) { 98 | writeReq := &api.WriteRequest{ 99 | Ext: r.extID, 100 | Net: r.netID, 101 | Msg: b, 102 | } 103 | 104 | _, err = r.client.Write(context.Background(), writeReq) 105 | if err != nil { 106 | return 0, err 107 | } 108 | return len(b), nil 109 | } 110 | 111 | func newWriter(client api.ExtClient, extID, netID string) irc.Writer { 112 | return irc.Helper{ 113 | Writer: remoteIRCWriter{client: client, extID: extID, netID: netID}, 114 | } 115 | } 116 | 117 | // Listen for events and commands and dispatch them to handlers. It blocks 118 | // forever on its two listening goroutines. 119 | func (c *Client) Listen() error { 120 | var eventIDs, cmdIDs []uint64 121 | c.mut.RLock() 122 | for id := range c.events { 123 | eventIDs = append(eventIDs, id) 124 | } 125 | for id := range c.commands { 126 | cmdIDs = append(cmdIDs, id) 127 | } 128 | c.mut.RUnlock() 129 | 130 | evSub := &api.SubscriptionRequest{Ext: c.extension, Ids: eventIDs} 131 | cmdSub := &api.SubscriptionRequest{Ext: c.extension, Ids: cmdIDs} 132 | 133 | evStream, err := c.client.Events(context.Background(), evSub) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | cmdStream, err := c.client.Commands(context.Background(), cmdSub) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | wg := new(sync.WaitGroup) 144 | wg.Add(2) 145 | 146 | var evErr, cmdErr error 147 | 148 | go func() { 149 | for { 150 | ircEventResp, err := evStream.Recv() 151 | if err != nil { 152 | evErr = err 153 | break 154 | } 155 | 156 | writer := newWriter(c.client, c.extension, ircEventResp.Event.Net) 157 | 158 | c.mut.RLock() 159 | handler := c.events[ircEventResp.Id] 160 | c.mut.RUnlock() 161 | 162 | if handler == nil { 163 | // How did this happen? 164 | continue 165 | } 166 | 167 | ev := &irc.Event{ 168 | Name: ircEventResp.Event.Name, 169 | Sender: ircEventResp.Event.Sender, 170 | Args: ircEventResp.Event.Args, 171 | Time: time.Unix(ircEventResp.Event.Time, 0), 172 | NetworkID: ircEventResp.Event.Net, 173 | } 174 | 175 | go handler.Handle(writer, ev) 176 | } 177 | 178 | wg.Done() 179 | }() 180 | 181 | go func() { 182 | for { 183 | cmdEventResp, err := cmdStream.Recv() 184 | if err != nil { 185 | cmdErr = err 186 | break 187 | } 188 | 189 | writer := newWriter(c.client, c.extension, cmdEventResp.Event.IrcEvent.Net) 190 | 191 | c.mut.RLock() 192 | handler := c.commands[cmdEventResp.Id] 193 | c.mut.RUnlock() 194 | 195 | if handler == nil { 196 | // How did this happen? 197 | continue 198 | } 199 | 200 | ircEvent := cmdEventResp.Event.IrcEvent 201 | iev := &irc.Event{ 202 | Name: ircEvent.Name, 203 | Sender: ircEvent.Sender, 204 | Args: ircEvent.Args, 205 | Time: time.Unix(ircEvent.Time, 0), 206 | NetworkID: ircEvent.Net, 207 | } 208 | 209 | ev := &cmd.Event{ 210 | Event: iev, 211 | Args: cmdEventResp.Event.Args, 212 | } 213 | 214 | go handler.Cmd(cmdEventResp.Name, writer, ev) 215 | } 216 | 217 | wg.Done() 218 | }() 219 | 220 | wg.Wait() 221 | 222 | if evErr != nil { 223 | return evErr 224 | } 225 | 226 | if cmdErr != nil { 227 | return cmdErr 228 | } 229 | 230 | return nil 231 | } 232 | 233 | // Register an event handler with the bot 234 | func (c *Client) Register(network string, channel string, event string, handler dispatch.Handler) (uint64, error) { 235 | req := &api.RegisterRequest{ 236 | Ext: c.extension, 237 | Network: network, 238 | Channel: channel, 239 | Event: event, 240 | } 241 | 242 | resp, err := c.client.Register(context.Background(), req) 243 | if err != nil { 244 | return 0, err 245 | } 246 | 247 | c.mut.Lock() 248 | c.events[resp.Id] = handler 249 | c.mut.Unlock() 250 | 251 | return resp.Id, nil 252 | } 253 | 254 | // RegisterCmd with the bot 255 | func (c *Client) RegisterCmd(network string, channel string, command *cmd.Command) (uint64, error) { 256 | req := &api.RegisterCmdRequest{ 257 | Ext: c.extension, 258 | Network: network, 259 | Channel: channel, 260 | Cmd: &api.Cmd{ 261 | Name: command.Name, 262 | Ext: command.Extension, 263 | Desc: command.Description, 264 | Kind: api.Cmd_Kind(command.Kind), 265 | Scope: api.Cmd_Scope(command.Scope), 266 | Args: command.Args, 267 | RequireAuth: command.RequireAuth, 268 | ReqLevel: int32(command.ReqLevel), 269 | ReqFlags: command.ReqFlags, 270 | }, 271 | } 272 | 273 | resp, err := c.client.RegisterCmd(context.Background(), req) 274 | if err != nil { 275 | return 0, err 276 | } 277 | 278 | c.mut.Lock() 279 | c.commands[resp.Id] = command.Handler 280 | c.mut.Unlock() 281 | 282 | return resp.Id, nil 283 | } 284 | 285 | // Unregister an event handler 286 | func (c *Client) Unregister(id uint64) (bool, error) { 287 | resp, err := c.client.Unregister(context.Background(), &api.UnregisterRequest{Id: id}) 288 | if err != nil { 289 | return false, err 290 | } 291 | 292 | c.mut.Lock() 293 | delete(c.events, id) 294 | c.mut.Unlock() 295 | 296 | return resp.Ok, nil 297 | } 298 | 299 | // UnregisterCmd from the bot 300 | func (c *Client) UnregisterCmd(id uint64) (bool, error) { 301 | resp, err := c.client.UnregisterCmd(context.Background(), &api.UnregisterRequest{Id: id}) 302 | if err != nil { 303 | return false, err 304 | } 305 | 306 | c.mut.Lock() 307 | delete(c.commands, id) 308 | c.mut.Unlock() 309 | 310 | return resp.Ok, nil 311 | } 312 | --------------------------------------------------------------------------------