├── .gitignore ├── .travis.yml ├── pkg ├── events │ ├── chatEvent.go │ ├── errorEvent.go │ └── definedEvents │ │ ├── definedErrorEvents.go │ │ └── definedChatEvents.go ├── chat │ ├── baseChannel.go │ ├── baseUser.go │ ├── mockAdapter │ │ ├── mockState │ │ │ └── mockState.go │ │ └── mockAdapter.go │ ├── baseMessage.go │ ├── adapter.go │ ├── shell │ │ └── shell.go │ └── slackRealtime │ │ └── slackRealtime.go └── store │ ├── adapter.go │ ├── memory │ └── memorystore.go │ └── boltstore │ ├── boltstore_test.go │ └── boltstore.go ├── LICENSE ├── handler.go ├── README.md ├── examples ├── simpleShell.go └── simpleSlack.go ├── robot.go ├── dispatch_test.go └── dispatch.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | - 1.4 5 | -------------------------------------------------------------------------------- /pkg/events/chatEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "fmt" 4 | 5 | type ChatEvent interface { 6 | fmt.Stringer 7 | } 8 | 9 | type BaseChatEvent struct { 10 | Text string 11 | } 12 | 13 | func (bc *BaseChatEvent) String() string { 14 | return bc.Text 15 | } 16 | -------------------------------------------------------------------------------- /pkg/chat/baseChannel.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | type BaseChannel struct { 4 | ChannelID, 5 | ChannelName string 6 | } 7 | 8 | func (b *BaseChannel) ID() string { 9 | return b.ChannelID 10 | } 11 | 12 | func (b *BaseChannel) Name() string { 13 | return b.ChannelName 14 | } 15 | -------------------------------------------------------------------------------- /pkg/store/adapter.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "fmt" 4 | 5 | var adapters = map[string]InitFunc{} 6 | 7 | func Register(name string, init InitFunc) { 8 | adapters[name] = init 9 | } 10 | 11 | func Load(name string) (InitFunc, error) { 12 | a, ok := adapters[name] 13 | 14 | if !ok { 15 | return nil, fmt.Errorf("unkown adapter: %s", name) 16 | } 17 | 18 | return a, nil 19 | } 20 | 21 | type InitFunc func(Robot) Adapter 22 | 23 | type Robot interface { 24 | Name() string 25 | StoreConfig() (interface{}, bool) 26 | } 27 | 28 | type Adapter interface { 29 | Get(string) (string, bool) 30 | Set(string, string) 31 | Delete(string) 32 | All() map[string]string 33 | } 34 | -------------------------------------------------------------------------------- /pkg/events/errorEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | type ErrorEvent interface { 4 | IsFatal() bool 5 | ErrorObject() error 6 | error 7 | } 8 | 9 | // BaseError provides a bare set/get implementation of the ErrorEvent 10 | // interface. 11 | type BaseError struct { 12 | ErrorObj error 13 | ErrorIsFatal bool 14 | } 15 | 16 | // Error returns the error event's underlying error's Error method. 17 | func (b *BaseError) Error() string { 18 | return b.ErrorObj.Error() 19 | } 20 | 21 | // ErrorObject returns the original error that this error event wraps. 22 | func (b *BaseError) ErrorObject() error { 23 | return b.ErrorObj 24 | } 25 | 26 | // IsFatal returns true if the error is unrecoverable by the chat adapter and 27 | // false otherwise. 28 | func (b *BaseError) IsFatal() bool { 29 | return b.ErrorIsFatal 30 | } 31 | -------------------------------------------------------------------------------- /pkg/chat/baseUser.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | // BaseUser provides a bare set/get implementation of the chat.User 4 | // interface that can be used by an adapter if it requires no additional logic 5 | // in its Users. 6 | type BaseUser struct { 7 | UserID, 8 | UserName, 9 | UserEmail string 10 | UserIsBot bool 11 | } 12 | 13 | // ID returns the User's chat ID. 14 | func (u *BaseUser) ID() string { 15 | return u.UserID 16 | } 17 | 18 | // Name returns the User's full name. 19 | func (u *BaseUser) Name() string { 20 | return u.UserName 21 | } 22 | 23 | // EmailAddress returns the user's email address in string form. 24 | func (u *BaseUser) EmailAddress() string { 25 | return u.UserEmail 26 | } 27 | 28 | // IsBot returns true if the user is a bot and false otherwise. 29 | func (u *BaseUser) IsBot() bool { 30 | return u.UserIsBot 31 | } 32 | -------------------------------------------------------------------------------- /pkg/store/memory/memorystore.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/FogCreek/victor/pkg/store" 7 | ) 8 | 9 | func init() { 10 | store.Register("memory", func(r store.Robot) store.Adapter { 11 | return &MemoryStore{ 12 | data: make(map[string]string), 13 | } 14 | }) 15 | } 16 | 17 | type MemoryStore struct { 18 | sync.RWMutex 19 | data map[string]string 20 | } 21 | 22 | func (s *MemoryStore) Get(key string) (string, bool) { 23 | s.RLock() 24 | defer s.RUnlock() 25 | val, ok := s.data[key] 26 | return val, ok 27 | } 28 | 29 | func (s *MemoryStore) Set(key string, val string) { 30 | s.Lock() 31 | defer s.Unlock() 32 | s.data[key] = val 33 | } 34 | 35 | func (s *MemoryStore) Delete(key string) { 36 | s.Lock() 37 | defer s.Unlock() 38 | delete(s.data, key) 39 | } 40 | 41 | func (s *MemoryStore) All() map[string]string { 42 | s.RLock() 43 | defer s.RUnlock() 44 | return s.data 45 | } 46 | -------------------------------------------------------------------------------- /pkg/chat/mockAdapter/mockState/mockState.go: -------------------------------------------------------------------------------- 1 | package mockState 2 | 3 | import ( 4 | "github.com/FogCreek/victor" 5 | "github.com/FogCreek/victor/pkg/chat" 6 | ) 7 | 8 | type MockState struct { 9 | MockRobot victor.Robot 10 | MockMessage *chat.BaseMessage 11 | MockFields []string 12 | } 13 | 14 | // Reply is a convience method to reply to the current message. 15 | // 16 | // Calling "state.Reply(msg) is equivalent to calling 17 | // "state.Chat().Send(state.Message().Channel().ID(), msg)" 18 | func (s *MockState) Reply(msg string) { 19 | s.MockRobot.Chat().Send(s.Message().Channel().ID(), msg) 20 | } 21 | 22 | // Robot returns the Robot. 23 | func (s *MockState) Robot() victor.Robot { 24 | return s.MockRobot 25 | } 26 | 27 | // Chat returns the Chat adapter. 28 | func (s *MockState) Chat() chat.Adapter { 29 | return s.MockRobot.Chat() 30 | } 31 | 32 | // Message returns the Message. 33 | func (s *MockState) Message() chat.Message { 34 | return s.MockMessage 35 | } 36 | 37 | // Fields returns the Message's Fields. 38 | func (s *MockState) Fields() []string { 39 | return s.MockFields 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Brett Buddin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /pkg/store/boltstore/boltstore_test.go: -------------------------------------------------------------------------------- 1 | package boltstore 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | DB_PATH = "test.db" 10 | ) 11 | 12 | var db *BoltStore 13 | 14 | func init() { 15 | os.Setenv("VICTOR_STORAGE_PATH", DB_PATH) 16 | } 17 | 18 | func setup() { 19 | os.Create(DB_PATH) 20 | if db == nil { 21 | db = newBoltStore() 22 | } 23 | } 24 | 25 | func teardown() { 26 | os.Remove(DB_PATH) 27 | } 28 | 29 | func TestEmptyGet(t *testing.T) { 30 | setup() 31 | 32 | val, _ := db.Get("nothing") 33 | if val != "" { 34 | t.Error("Expected to get nothing before store has data, got: ", val) 35 | } 36 | 37 | teardown() 38 | } 39 | 40 | func TestSet(t *testing.T) { 41 | setup() 42 | 43 | db.Set("a", "b") 44 | val, _ := db.Get("a") 45 | 46 | if val != "b" { 47 | t.Error("Stored 'a': 'b', expected to get it back", val) 48 | } 49 | 50 | teardown() 51 | } 52 | 53 | func TestDelete(t *testing.T) { 54 | setup() 55 | 56 | db.Set("a", "b") 57 | db.Delete("a") 58 | val, _ := db.Get("a") 59 | if val != "" { 60 | t.Error("Expected to get nothing after deleting key, got: ", val) 61 | } 62 | 63 | teardown() 64 | } 65 | -------------------------------------------------------------------------------- /pkg/chat/baseMessage.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | // BaseMessage provides a bare set/get implementation of the chat.Message 4 | // interface that can be used by an adapter if it requires no additional logic 5 | // in its Messages. 6 | type BaseMessage struct { 7 | MsgUser User 8 | MsgChannel Channel 9 | MsgText string 10 | MsgIsDirect bool 11 | MsgArchiveLink string 12 | MsgTimestamp string 13 | } 14 | 15 | // User gets the message's user. 16 | func (m *BaseMessage) User() User { 17 | return m.MsgUser 18 | } 19 | 20 | // Channel gets the message's channel object. 21 | func (m *BaseMessage) Channel() Channel { 22 | return m.MsgChannel 23 | } 24 | 25 | // Text gets the channel's text. 26 | func (m *BaseMessage) Text() string { 27 | return m.MsgText 28 | } 29 | 30 | // IsDirectMessage returns true if the message was direct (private) and false 31 | // otherwise. 32 | func (m *BaseMessage) IsDirectMessage() bool { 33 | return m.MsgIsDirect 34 | } 35 | 36 | // ArchiveLink gets the message's archive link. 37 | func (m *BaseMessage) ArchiveLink() string { 38 | return m.MsgArchiveLink 39 | } 40 | 41 | // Timestamp gets the message's timestamp. 42 | func (m *BaseMessage) Timestamp() string { 43 | return m.MsgTimestamp 44 | } 45 | -------------------------------------------------------------------------------- /pkg/events/definedEvents/definedErrorEvents.go: -------------------------------------------------------------------------------- 1 | package definedEvents 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | type InvalidAuth struct{} 9 | 10 | func (i *InvalidAuth) Error() string { 11 | return i.ErrorObject().Error() 12 | } 13 | 14 | func (i *InvalidAuth) ErrorObject() error { 15 | return errors.New("Invalid Auth") 16 | } 17 | 18 | func (i *InvalidAuth) IsFatal() bool { 19 | return true 20 | } 21 | 22 | type Disconnect struct { 23 | Intentional bool 24 | } 25 | 26 | func (d *Disconnect) Error() string { 27 | return d.ErrorObject().Error() 28 | } 29 | 30 | func (d *Disconnect) IsFatal() bool { 31 | return false 32 | } 33 | 34 | func (d *Disconnect) ErrorObject() error { 35 | if d.Intentional { 36 | return errors.New("Intentional Disconnect") 37 | } else { 38 | return errors.New("Unexpected Disconnect") 39 | } 40 | } 41 | 42 | type MessageTooLong struct { 43 | ChannelID string 44 | Text string 45 | MaxLength int 46 | } 47 | 48 | func (m *MessageTooLong) Error() string { 49 | return m.ErrorObject().Error() 50 | } 51 | 52 | func (m *MessageTooLong) ErrorObject() error { 53 | return fmt.Errorf("Message too long (max %d chars)", m.MaxLength) 54 | } 55 | 56 | func (m *MessageTooLong) IsFatal() bool { 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package victor 2 | 3 | import ( 4 | "github.com/FogCreek/victor/pkg/chat" 5 | ) 6 | 7 | // Handler defines an interface for a message handler 8 | type Handler interface { 9 | Handle(State) 10 | } 11 | 12 | // HandlerFunc defines the parameters for a handler's function 13 | type HandlerFunc func(State) 14 | 15 | // Handle calls the handler's response function which replies to a message 16 | // appropriately. 17 | func (f HandlerFunc) Handle(s State) { 18 | f(s) 19 | } 20 | 21 | // State defines an interface to provide a handler all of the necessary 22 | // information to reply to a message 23 | type State interface { 24 | Robot() Robot 25 | Chat() chat.Adapter 26 | Message() chat.Message 27 | Fields() []string 28 | Reply(string) 29 | } 30 | 31 | type state struct { 32 | robot Robot 33 | message chat.Message 34 | fields []string 35 | } 36 | 37 | // Reply is a convience method to reply to the current message. 38 | // 39 | // Calling "state.Reply(msg) is equivalent to calling 40 | // "state.Chat().Send(state.Message().Channel().ID(), msg)" 41 | func (s *state) Reply(msg string) { 42 | s.robot.Chat().Send(s.message.Channel().ID(), msg) 43 | } 44 | 45 | // Returns the Robot 46 | func (s *state) Robot() Robot { 47 | return s.robot 48 | } 49 | 50 | // Returns the Chat adapter 51 | func (s *state) Chat() chat.Adapter { 52 | return s.robot.Chat() 53 | } 54 | 55 | // Returns the Message 56 | func (s *state) Message() chat.Message { 57 | return s.message 58 | } 59 | 60 | func (s *state) Fields() []string { 61 | return s.fields 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Victor 2 | 3 | **Victor** is a library for creating your own chat bot. 4 | 5 | Victor is a fork of [brettbuddin/victor](https://github.com/brettbuddin/victor) with several breaking changes to routing. 6 | 7 | ### Supported Services 8 | 9 | Currently there is a chat adatper for the [Slack Real Time API](https://api.slack.com/rtm). There are other adapters on the [original repo](https://github.com/brettbuddin/victor) that will need some modification due to the breaking changes of this fork. 10 | 11 | One breaking change is the addition of two chat event-driven channels which are handled by the Robot interface in the methods `robot.ChatErrors()` and `robot.ChatEvents()`. These channels must be "listened" to or over time there will be many goroutines waiting on blocking sends. The Slack Real Time adapter is designed such that all sends to these channels are performed on goroutines and will therefore continue to work. Ignoring the channels is not recommended and simply receiving the events pushed to them and ignoring them will suffice. For an example, look at the [examples](https://github.com/FogCreek/victor/tree/master/examples). 12 | 13 | * **Slack Real Time** 14 | To use victor with the slack real time adapter, you need to [add a new bot](https://my.slack.com/services/new/bot) and initialize victor with an adapterConfig struct that matches the victor/pkg/chat/slackRealtime.Config interface to return its token. 15 | 16 | At the moment the bot's `Stop` method is broken with this adapter! 17 | 18 | 19 | A simple example is located in [examples](https://github.com/FogCreek/victor/tree/master/examples). 20 | -------------------------------------------------------------------------------- /pkg/chat/adapter.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/FogCreek/victor/pkg/events" 7 | "github.com/FogCreek/victor/pkg/store" 8 | ) 9 | 10 | var adapters = map[string]InitFunc{} 11 | 12 | func Register(name string, init InitFunc) { 13 | adapters[name] = init 14 | } 15 | 16 | func Load(name string) (InitFunc, error) { 17 | a, ok := adapters[name] 18 | 19 | if !ok { 20 | return nil, fmt.Errorf("unkown adapter: %s", name) 21 | } 22 | 23 | return a, nil 24 | } 25 | 26 | type InitFunc func(Robot) Adapter 27 | 28 | type Adapter interface { 29 | Run() 30 | Send(string, string) 31 | SendDirectMessage(string, string) 32 | SendTyping(string) 33 | Stop() 34 | // ID should return a unique ID for that adapter which is guarenteed to 35 | // remain constant as long as the adapter points to the same chat instance. 36 | ID() string 37 | GetUser(string) User 38 | GetChannel(string) Channel 39 | IsPotentialUser(string) bool 40 | IsPotentialChannel(string) bool 41 | GetAllUsers() []User 42 | GetBot() User 43 | GetPublicChannels() []Channel 44 | GetGeneralChannel() Channel 45 | 46 | // Name returns the name of the team/chat instance. 47 | Name() string 48 | MaxLength() int 49 | } 50 | 51 | type Robot interface { 52 | Name() string 53 | RefreshUserName() 54 | Store() store.Adapter 55 | Chat() Adapter 56 | Receive(Message) 57 | AdapterConfig() (interface{}, bool) 58 | ChatErrors() chan events.ErrorEvent 59 | ChatEvents() chan events.ChatEvent 60 | } 61 | 62 | type Message interface { 63 | User() User 64 | Channel() Channel 65 | Text() string 66 | IsDirectMessage() bool 67 | ArchiveLink() string 68 | Timestamp() string 69 | } 70 | 71 | type User interface { 72 | ID() string 73 | Name() string 74 | EmailAddress() string 75 | IsBot() bool 76 | } 77 | 78 | type Channel interface { 79 | Name() string 80 | ID() string 81 | } 82 | -------------------------------------------------------------------------------- /pkg/events/definedEvents/definedChatEvents.go: -------------------------------------------------------------------------------- 1 | package definedEvents 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/FogCreek/victor/pkg/chat" 7 | ) 8 | 9 | type ConnectingEvent struct{} 10 | 11 | func (c *ConnectingEvent) String() string { 12 | return "Connecting" 13 | } 14 | 15 | type ConnectedEvent struct{} 16 | 17 | func (c *ConnectedEvent) String() string { 18 | return "Connected" 19 | } 20 | 21 | type UserEvent struct { 22 | User chat.User 23 | WasRemoved bool 24 | } 25 | 26 | func (u *UserEvent) String() string { 27 | userPart := fmt.Sprintf("User %s (%s) was ", u.User.Name(), u.User.ID()) 28 | if u.WasRemoved { 29 | return userPart + "removed" 30 | } 31 | return userPart + "added" 32 | } 33 | 34 | type UserChangedEvent struct { 35 | User chat.User 36 | OldName string 37 | OldEmailAddress string 38 | } 39 | 40 | const changeFmt = `"%s" --> "%s"` 41 | 42 | func (u *UserChangedEvent) String() string { 43 | if len(u.OldEmailAddress) > 0 && len(u.OldName) > 0 { 44 | return fmt.Sprintf("User %s changed: email "+changeFmt+ 45 | " & name "+changeFmt, 46 | u.User.ID(), u.OldEmailAddress, u.User.EmailAddress(), 47 | u.OldName, u.User.Name()) 48 | } else if len(u.OldName) > 0 { 49 | return fmt.Sprintf("User %s changed: name "+changeFmt, 50 | u.User.ID(), u.OldName, u.User.Name()) 51 | } else if len(u.OldEmailAddress) > 0 { 52 | return fmt.Sprintf("User %s changed: name "+changeFmt, 53 | u.User.ID(), u.OldEmailAddress, u.User.EmailAddress()) 54 | } else { 55 | return fmt.Sprintf("User %d did not change", u.User.ID()) 56 | } 57 | } 58 | 59 | type ChannelEvent struct { 60 | Channel chat.Channel 61 | WasRemoved bool 62 | } 63 | 64 | func (c *ChannelEvent) String() string { 65 | channelPart := fmt.Sprintf("Channel %s (%s) was ", c.Channel.ID(), c.Channel.Name()) 66 | if c.WasRemoved { 67 | return channelPart + "removed" 68 | } 69 | return channelPart + "added" 70 | } 71 | 72 | type ChannelChangedEvent struct { 73 | Channel chat.Channel 74 | OldName string 75 | } 76 | 77 | func (c *ChannelChangedEvent) String() string { 78 | return fmt.Sprintf("Channel %s has changed from \"%s\" to \"%s\"", 79 | c.Channel.ID(), c.OldName, c.Channel.Name()) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/store/boltstore/boltstore.go: -------------------------------------------------------------------------------- 1 | package boltstore 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/FogCreek/victor/pkg/store" 9 | "github.com/boltdb/bolt" 10 | ) 11 | 12 | var _ = fmt.Println 13 | 14 | const ( 15 | defaultBucket = "victor" 16 | ) 17 | 18 | func init() { 19 | // type InitFunc func() Adapter 20 | store.Register("bolt", func(r store.Robot) store.Adapter { 21 | return newBoltStore() 22 | }) 23 | } 24 | 25 | func newBoltStore() *BoltStore { 26 | return &BoltStore{ 27 | defaultBucket: []byte(defaultBucket), 28 | } 29 | } 30 | 31 | type BoltStore struct { 32 | defaultBucket []byte 33 | DB *bolt.DB 34 | } 35 | 36 | func (s *BoltStore) withDB(callback func(db *bolt.DB) error) error { 37 | dbPath := os.Getenv("VICTOR_STORAGE_PATH") 38 | 39 | db, err := bolt.Open(dbPath, 0600, nil) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | defer db.Close() 44 | 45 | db.Update(func(tx *bolt.Tx) error { 46 | tx.CreateBucketIfNotExists([]byte(s.defaultBucket)) 47 | return nil 48 | }) 49 | 50 | return callback(db) 51 | } 52 | 53 | func (s *BoltStore) update(callback func(b *bolt.Bucket) error) error { 54 | return s.withDB(func(db *bolt.DB) error { 55 | return db.Update(func(tx *bolt.Tx) error { 56 | b := tx.Bucket(s.defaultBucket) 57 | return callback(b) 58 | }) 59 | }) 60 | } 61 | 62 | func (s *BoltStore) view(callback func(b *bolt.Bucket) error) error { 63 | return s.withDB(func(db *bolt.DB) error { 64 | return db.View(func(tx *bolt.Tx) error { 65 | b := tx.Bucket(s.defaultBucket) 66 | return callback(b) 67 | }) 68 | }) 69 | } 70 | 71 | func (s *BoltStore) Get(key string) (string, bool) { 72 | var val string 73 | 74 | err := s.view(func(b *bolt.Bucket) error { 75 | bval := b.Get([]byte(key)) 76 | 77 | if bval != nil { 78 | val = string(bval) 79 | } 80 | 81 | return nil 82 | }) 83 | 84 | if err != nil { 85 | log.Println("[boltdb Get] error getting", key, "-", err) 86 | } 87 | 88 | return val, (val == "") 89 | } 90 | 91 | func (s *BoltStore) Set(key string, val string) { 92 | bkey := []byte(key) 93 | 94 | err := s.update(func(b *bolt.Bucket) error { 95 | return b.Put(bkey, []byte(val)) 96 | }) 97 | 98 | if err != nil { 99 | log.Println("[boltdb Set] error setting", key, "-", err) 100 | } 101 | } 102 | 103 | func (s *BoltStore) Delete(key string) { 104 | err := s.update(func(b *bolt.Bucket) error { 105 | err := b.Delete([]byte(key)) 106 | return err 107 | }) 108 | 109 | if err != nil { 110 | log.Println("[boltdb Delete] error deleting", key, "-", err) 111 | } 112 | } 113 | 114 | func (s *BoltStore) All() map[string]string { 115 | // nope 116 | return make(map[string]string) 117 | } 118 | -------------------------------------------------------------------------------- /examples/simpleShell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/FogCreek/victor" 10 | "github.com/FogCreek/victor/pkg/events" 11 | ) 12 | 13 | const BOT_NAME = "victor" 14 | 15 | func main() { 16 | bot := victor.New(victor.Config{ 17 | ChatAdapter: "shell", 18 | Name: BOT_NAME, 19 | }) 20 | addHandlers(bot) 21 | // optional help built in help command 22 | bot.EnableHelpCommand() 23 | bot.Run() 24 | go monitorErrors(bot.ChatErrors()) 25 | go monitorEvents(bot.ChatEvents()) 26 | // keep the process (and bot) alive 27 | sigs := make(chan os.Signal, 1) 28 | signal.Notify(sigs, os.Interrupt) 29 | <-sigs 30 | 31 | bot.Stop() 32 | } 33 | 34 | func monitorErrors(errorChannel chan events.ErrorEvent) { 35 | for { 36 | e, ok := <-errorChannel 37 | if !ok { 38 | return 39 | } 40 | log.Println(e) 41 | } 42 | } 43 | 44 | func monitorEvents(eventsChannel chan events.ChatEvent) { 45 | for { 46 | e, ok := <-eventsChannel 47 | if !ok { 48 | return 49 | } 50 | log.Println(e) 51 | } 52 | } 53 | 54 | func addHandlers(r victor.Robot) { 55 | // Add a typical command that will be displayed using the "help" command 56 | // if it is enabled. 57 | r.HandleCommand(&victor.HandlerDoc{ 58 | CmdHandler: byeFunc, 59 | CmdName: "hi", 60 | CmdDescription: "Says goodbye when the user says hi!", 61 | CmdUsage: []string{""}, 62 | }) 63 | // Add a hidden command that isn't displayed in the "help" command unless 64 | // mentioned by name 65 | r.HandleCommand(&victor.HandlerDoc{ 66 | CmdHandler: echoFunc, 67 | CmdName: "echo", 68 | CmdDescription: "Hidden `echo` command!", 69 | CmdUsage: []string{"", "`text to echo`"}, 70 | CmdIsHidden: true, 71 | }) 72 | // Add a command to show the "Fields" method 73 | r.HandleCommand(&victor.HandlerDoc{ 74 | CmdHandler: fieldsFunc, 75 | CmdName: "fields", 76 | CmdDescription: "Show the fields/parameters of a command message!", 77 | CmdUsage: []string{"`param0` `param1` `...`"}, 78 | }) 79 | // Add a general pattern which is only checked on the first word of 80 | // "command" messages which are described in dispatch.go 81 | r.HandleCommandPattern("thank[s]?(\\s+you)?", &victor.HandlerDoc{ 82 | CmdHandler: thanksFunc, 83 | CmdName: "thanks", 84 | CmdDescription: "Say thank you!", 85 | }) 86 | // Add default handler to show "unrecognized command" on "command" messages 87 | r.SetDefaultHandler(defaultFunc) 88 | } 89 | 90 | func byeFunc(s victor.State) { 91 | msg := fmt.Sprintf("Bye %s!", s.Message().User().Name()) 92 | s.Chat().Send(s.Message().Channel().ID(), msg) 93 | } 94 | 95 | func echoFunc(s victor.State) { 96 | s.Chat().Send(s.Message().Channel().ID(), s.Message().Text()) 97 | } 98 | 99 | func thanksFunc(s victor.State) { 100 | msg := fmt.Sprintf("You're welcome %s!", s.Message().User().Name()) 101 | s.Chat().Send(s.Message().Channel().ID(), msg) 102 | } 103 | 104 | func fieldsFunc(s victor.State) { 105 | for _, f := range s.Fields() { 106 | s.Chat().Send(s.Message().Channel().ID(), f) 107 | } 108 | } 109 | 110 | func defaultFunc(s victor.State) { 111 | s.Chat().Send(s.Message().Channel().ID(), 112 | "Unrecognized command. Type `help` to see supported commands.") 113 | } 114 | -------------------------------------------------------------------------------- /pkg/chat/shell/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/FogCreek/victor/pkg/chat" 12 | "github.com/FogCreek/victor/pkg/events" 13 | ) 14 | 15 | const ( 16 | timeFormat = "20060102150405" 17 | chatNameFormat = "Shell Instance %d" 18 | ) 19 | 20 | var ( 21 | realUser = &chat.BaseUser{ 22 | UserID: "shell_user", 23 | UserName: "[Shell User]", 24 | UserEmail: "user@example.com", 25 | UserIsBot: false, 26 | } 27 | defaultChannel = &chat.BaseChannel{ 28 | ChannelID: "shell_channel", 29 | ChannelName: "shell_channel", 30 | } 31 | nextID = 0 32 | nextIDMutex = &sync.Mutex{} 33 | ) 34 | 35 | func init() { 36 | chat.Register("shell", func(r chat.Robot) chat.Adapter { 37 | nextIDMutex.Lock() 38 | id := strconv.Itoa(nextID) 39 | nextID++ 40 | nextIDMutex.Unlock() 41 | return &Adapter{ 42 | robot: r, 43 | stop: make(chan bool), 44 | id: id, 45 | lines: make(chan string), 46 | botUser: &chat.BaseUser{ 47 | UserID: id, 48 | UserName: "unknown", 49 | UserIsBot: true, 50 | }, 51 | } 52 | }) 53 | } 54 | 55 | type Adapter struct { 56 | robot chat.Robot 57 | stop chan bool 58 | id string 59 | lines chan string 60 | botUser chat.User 61 | } 62 | 63 | func (a *Adapter) MaxLength() int { 64 | return -1 65 | } 66 | 67 | func (a *Adapter) Run() { 68 | reader := bufio.NewReader(os.Stdin) 69 | 70 | go func() { 71 | for { 72 | if line, _, err := reader.ReadLine(); err == nil { 73 | a.lines <- string(line) 74 | } else { 75 | a.robot.ChatErrors() <- &events.BaseError{ 76 | ErrorObj: err, 77 | } 78 | } 79 | } 80 | }() 81 | go a.monitorEvents() 82 | } 83 | 84 | func (a *Adapter) monitorEvents() { 85 | for { 86 | select { 87 | case <-a.stop: 88 | return 89 | case line := <-a.lines: 90 | a.robot.Receive(&chat.BaseMessage{ 91 | MsgText: string(line), 92 | MsgUser: realUser, 93 | MsgChannel: defaultChannel, 94 | MsgIsDirect: true, 95 | MsgArchiveLink: "", 96 | MsgTimestamp: time.Now().Format(timeFormat), 97 | }) 98 | } 99 | } 100 | } 101 | 102 | func (a *Adapter) Name() string { 103 | return fmt.Sprintf(chatNameFormat, a.id) 104 | } 105 | 106 | func (a *Adapter) Send(channelID, msg string) { 107 | fmt.Println("SEND:", msg) 108 | } 109 | 110 | func (a *Adapter) SendDirectMessage(userID, msg string) { 111 | a.Send("", "DIRECT MESSAGE: "+msg) 112 | } 113 | 114 | func (a *Adapter) SendTyping(string) { 115 | return 116 | } 117 | 118 | func (a *Adapter) Stop() { 119 | a.stop <- true 120 | close(a.stop) 121 | } 122 | 123 | func (a *Adapter) ID() string { 124 | return a.id 125 | } 126 | 127 | func (a *Adapter) GetUser(userID string) chat.User { 128 | if userID == realUser.ID() { 129 | return realUser 130 | } 131 | return nil 132 | } 133 | 134 | func (a *Adapter) GetBot() chat.User { 135 | return a.botUser 136 | } 137 | 138 | func (a *Adapter) GetChannel(channelID string) chat.Channel { 139 | if channelID == defaultChannel.ID() { 140 | return defaultChannel 141 | } 142 | return nil 143 | } 144 | 145 | func (a *Adapter) GetGeneralChannel() chat.Channel { 146 | return defaultChannel 147 | } 148 | 149 | func (a *Adapter) GetAllUsers() []chat.User { 150 | return []chat.User{realUser} 151 | } 152 | 153 | func (a *Adapter) GetPublicChannels() []chat.Channel { 154 | return []chat.Channel{defaultChannel} 155 | } 156 | 157 | func (a *Adapter) IsPotentialUser(userID string) bool { 158 | return userID == realUser.ID() 159 | } 160 | 161 | func (a *Adapter) IsPotentialChannel(channelID string) bool { 162 | return channelID == defaultChannel.ChannelID 163 | } 164 | 165 | func (a *Adapter) NormalizeUserID(userID string) string { 166 | return userID 167 | } 168 | -------------------------------------------------------------------------------- /examples/simpleSlack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/FogCreek/victor" 10 | "github.com/FogCreek/victor/pkg/chat/slackRealtime" 11 | "github.com/FogCreek/victor/pkg/events" 12 | "github.com/FogCreek/victor/pkg/events/definedEvents" 13 | ) 14 | 15 | const SLACK_TOKEN = "SLACK_TOKEN" 16 | const BOT_NAME = "BOT_NAME" 17 | 18 | func main() { 19 | 20 | defer func() { 21 | // this is only necessary since the slack api used by the slack adapter 22 | // does not currently implement a "Stop" or "Disconnect" method 23 | if e := recover(); e != nil { 24 | fmt.Println("bot.Stop() exited with panic: ", e) 25 | os.Exit(1) 26 | } 27 | }() 28 | 29 | bot := victor.New(victor.Config{ 30 | ChatAdapter: "slackRealtime", 31 | AdapterConfig: slackRealtime.NewConfig(SLACK_TOKEN), 32 | Name: BOT_NAME, 33 | }) 34 | addHandlers(bot) 35 | // optional help built in help command 36 | bot.EnableHelpCommand() 37 | bot.Run() 38 | go monitorErrors(bot.ChatErrors()) 39 | go monitorEvents(bot.ChatEvents()) 40 | // keep the process (and bot) alive 41 | sigs := make(chan os.Signal, 1) 42 | signal.Notify(sigs, os.Interrupt) 43 | <-sigs 44 | 45 | bot.Stop() 46 | } 47 | 48 | func monitorErrors(errorChannel <-chan events.ErrorEvent) { 49 | for { 50 | err, ok := <-errorChannel 51 | if !ok { 52 | return 53 | } 54 | if err.IsFatal() { 55 | log.Panic(err.Error()) 56 | } 57 | log.Println("Chat Adapter Error Event:", err.Error()) 58 | } 59 | } 60 | 61 | func monitorEvents(eventsChannel chan events.ChatEvent) { 62 | for { 63 | event, ok := <-eventsChannel 64 | if !ok { 65 | return 66 | } 67 | switch e := event.(type) { 68 | case *definedEvents.ConnectingEvent: 69 | log.Println("Connecting Event fired") 70 | case *definedEvents.ConnectedEvent: 71 | log.Println("Connected Event fired") 72 | case *definedEvents.UserEvent: 73 | log.Printf("User Event: %+v", e) 74 | case *definedEvents.ChannelEvent: 75 | log.Printf("Channel Event: %+v", e) 76 | default: 77 | log.Println("Unrecognized Chat Event:", e) 78 | } 79 | } 80 | } 81 | 82 | func addHandlers(r victor.Robot) { 83 | // Add a typical command that will be displayed using the "help" command 84 | // if it is enabled. 85 | r.HandleCommand(&victor.HandlerDoc{ 86 | CmdHandler: byeFunc, 87 | CmdName: "hi", 88 | CmdDescription: "Says goodbye when the user says hi!", 89 | CmdUsage: []string{""}, 90 | }) 91 | // Add a hidden command that isn't displayed in the "help" command unless 92 | // mentioned by name 93 | r.HandleCommand(&victor.HandlerDoc{ 94 | CmdHandler: echoFunc, 95 | CmdName: "echo", 96 | CmdDescription: "Hidden `echo` command!", 97 | CmdUsage: []string{"", "`text to echo`"}, 98 | CmdIsHidden: true, 99 | }) 100 | // Add a command to show the "Fields" method 101 | r.HandleCommand(&victor.HandlerDoc{ 102 | CmdHandler: fieldsFunc, 103 | CmdName: "fields", 104 | CmdDescription: "Show the fields/parameters of a command message!", 105 | CmdUsage: []string{"`param0` `param1` `...`"}, 106 | }) 107 | // Add a general pattern which is only checked on "non-command" messages 108 | // which are described in dispatch.go 109 | r.HandlePattern("\b(thanks|thank\\s+you)\b", thanksFunc) 110 | // Add default handler to show "unrecognized command" on "command" messages 111 | r.SetDefaultHandler(defaultFunc) 112 | } 113 | 114 | func byeFunc(s victor.State) { 115 | msg := fmt.Sprintf("Bye %s!", s.Message().User().Name()) 116 | s.Chat().Send(s.Message().Channel().ID(), msg) 117 | } 118 | 119 | func echoFunc(s victor.State) { 120 | s.Chat().Send(s.Message().Channel().ID(), s.Message().Text()) 121 | } 122 | 123 | func thanksFunc(s victor.State) { 124 | msg := fmt.Sprintf("You're welcome %s!", s.Message().User().Name()) 125 | s.Chat().Send(s.Message().Channel().ID(), msg) 126 | } 127 | 128 | func fieldsFunc(s victor.State) { 129 | var fStr string 130 | for _, f := range s.Fields() { 131 | fStr += f + "\n" 132 | } 133 | s.Chat().Send(s.Message().Channel().ID(), fStr) 134 | } 135 | 136 | func defaultFunc(s victor.State) { 137 | s.Chat().Send(s.Message().Channel().ID(), 138 | "Unrecognized command. Type `help` to see supported commands.") 139 | } 140 | -------------------------------------------------------------------------------- /robot.go: -------------------------------------------------------------------------------- 1 | package victor 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/FogCreek/victor/pkg/chat" 11 | "github.com/FogCreek/victor/pkg/events" 12 | // Blank import used init adapters which registers them with victor 13 | _ "github.com/FogCreek/victor/pkg/chat/shell" 14 | _ "github.com/FogCreek/victor/pkg/chat/slackRealtime" 15 | "github.com/FogCreek/victor/pkg/store" 16 | // Blank import used init adapters which registers them with victor 17 | _ "github.com/FogCreek/victor/pkg/store/boltstore" 18 | _ "github.com/FogCreek/victor/pkg/store/memory" 19 | ) 20 | 21 | // Robot provides an interface for a victor chat robot. 22 | type Robot interface { 23 | Run() 24 | Stop() 25 | Name() string 26 | RefreshUserName() 27 | HandleCommand(HandlerDocPair) 28 | HandleCommandPattern(string, HandlerDocPair) 29 | HandleCommandRegexp(*regexp.Regexp, HandlerDocPair) 30 | HandleCommandAlias(string, string) 31 | HandleCommandAliasPattern(string, string, string) 32 | HandleCommandAliasRegexp(string, string, *regexp.Regexp) 33 | HandlePattern(string, HandlerFunc) 34 | HandleRegexp(*regexp.Regexp, HandlerFunc) 35 | SetDefaultHandler(HandlerFunc) 36 | EnableHelpCommand() 37 | Commands() map[string]HandlerDocPair 38 | Receive(chat.Message) 39 | Chat() chat.Adapter 40 | Store() store.Adapter 41 | AdapterConfig() (interface{}, bool) 42 | StoreConfig() (interface{}, bool) 43 | ChatErrors() chan events.ErrorEvent 44 | ChatEvents() chan events.ChatEvent 45 | } 46 | 47 | // Config provides all of the configuration parameters needed in order to 48 | // initialize a robot. It also allows for optional configuration structs for 49 | // both the chat and storage adapters which they may or may not require. 50 | type Config struct { 51 | Name, 52 | ChatAdapter, 53 | StoreAdapter string 54 | AdapterConfig, 55 | StoreConfig interface{} 56 | } 57 | 58 | type robot struct { 59 | *dispatch 60 | name string 61 | store store.Adapter 62 | chat chat.Adapter 63 | incoming chan chat.Message 64 | stop chan struct{} 65 | adapterConfig, 66 | storeConfig interface{} 67 | chatErrorChannel chan events.ErrorEvent 68 | chatEventChannel chan events.ChatEvent 69 | } 70 | 71 | // New returns a robot 72 | func New(config Config) *robot { 73 | chatAdapter := config.ChatAdapter 74 | if chatAdapter == "" { 75 | log.Println("Shell adapter has been removed.") 76 | chatAdapter = "shell" 77 | } 78 | 79 | chatInitFunc, err := chat.Load(config.ChatAdapter) 80 | 81 | if err != nil { 82 | log.Println(err) 83 | os.Exit(1) 84 | } 85 | 86 | storeAdapter := config.StoreAdapter 87 | if storeAdapter == "" { 88 | storeAdapter = "memory" 89 | } 90 | 91 | storeInitFunc, err := store.Load(storeAdapter) 92 | 93 | if err != nil { 94 | log.Println(err) 95 | os.Exit(1) 96 | } 97 | 98 | botName := config.Name 99 | if botName == "" { 100 | botName = "victor" 101 | } 102 | 103 | bot := &robot{ 104 | incoming: make(chan chat.Message), 105 | stop: make(chan struct{}), 106 | chatErrorChannel: make(chan events.ErrorEvent), 107 | chatEventChannel: make(chan events.ChatEvent), 108 | adapterConfig: config.AdapterConfig, 109 | } 110 | 111 | bot.store = storeInitFunc(bot) 112 | bot.chat = chatInitFunc(bot) 113 | bot.dispatch = newDispatch(bot) 114 | return bot 115 | } 116 | 117 | // Receive accepts messages for processing 118 | func (r *robot) Receive(m chat.Message) { 119 | r.incoming <- m 120 | } 121 | 122 | // Run starts the robot. 123 | func (r *robot) Run() { 124 | r.chat.Run() 125 | 126 | go func() { 127 | for { 128 | select { 129 | case <-r.stop: 130 | close(r.incoming) 131 | return 132 | case m := <-r.incoming: 133 | if strings.ToLower(m.User().Name()) != r.name { 134 | go r.ProcessMessage(m) 135 | } 136 | } 137 | } 138 | }() 139 | } 140 | 141 | // Stop shuts down the bot 142 | func (r *robot) Stop() { 143 | r.chat.Stop() 144 | close(r.stop) 145 | } 146 | 147 | // Name returns the name of the bot 148 | func (r *robot) Name() string { 149 | return r.Chat().GetBot().Name() 150 | } 151 | 152 | // Store returns the data store adapter 153 | func (r *robot) Store() store.Adapter { 154 | return r.store 155 | } 156 | 157 | // Chat returns the chat adapter 158 | func (r *robot) Chat() chat.Adapter { 159 | return r.chat 160 | } 161 | 162 | func (r *robot) AdapterConfig() (interface{}, bool) { 163 | return r.adapterConfig, r.adapterConfig != nil 164 | } 165 | 166 | func (r *robot) StoreConfig() (interface{}, bool) { 167 | return r.storeConfig, r.storeConfig != nil 168 | } 169 | 170 | func (r *robot) ChatErrors() chan events.ErrorEvent { 171 | return r.chatErrorChannel 172 | } 173 | 174 | func (r *robot) ChatEvents() chan events.ChatEvent { 175 | return r.chatEventChannel 176 | } 177 | 178 | // OnlyAllow provides a way of permitting specific users 179 | // to execute a handler registered with the bot 180 | func OnlyAllow(userNames []string, action func(s State)) func(State) { 181 | return func(s State) { 182 | actual := s.Message().User().Name() 183 | for _, name := range userNames { 184 | if name == actual { 185 | action(s) 186 | return 187 | } 188 | } 189 | 190 | s.Chat().Send(s.Message().Channel().ID(), fmt.Sprintf("Sorry, %s. I can't let you do that.", actual)) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /pkg/chat/mockAdapter/mockAdapter.go: -------------------------------------------------------------------------------- 1 | package mockAdapter 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | 7 | "github.com/FogCreek/victor/pkg/chat" 8 | ) 9 | 10 | var ( 11 | nextID = 0 12 | nextIDMutex = &sync.Mutex{} 13 | defaultUserRet = &chat.BaseUser{ 14 | UserName: "Fake User", 15 | UserID: "UFakeUser", 16 | UserIsBot: false, 17 | UserEmail: "fake@example.com", 18 | } 19 | defaultChannelRet = &chat.BaseChannel{ 20 | ChannelName: "Fake Channel", 21 | ChannelID: "CFakeChannel", 22 | } 23 | ) 24 | 25 | // init Registers the mockAdapter with the victor framework under the chat 26 | // adapter name "mockAdapter". 27 | func init() { 28 | chat.Register("mockAdapter", func(r chat.Robot) chat.Adapter { 29 | nextIDMutex.Lock() 30 | id := strconv.Itoa(nextID) 31 | nextID++ 32 | nextIDMutex.Unlock() 33 | return &MockChatAdapter{ 34 | robot: r, 35 | id: id, 36 | Sent: make([]MockMessagePair, 0, 10), 37 | SentPublic: make([]MockMessagePair, 0, 10), 38 | SentDirect: make([]MockMessagePair, 0, 10), 39 | IsPotentialUserRet: true, 40 | IsPotentialChannelRet: true, 41 | UserRet: defaultUserRet, 42 | ChannelRet: defaultChannelRet, 43 | GeneralChannelRet: defaultChannelRet, 44 | AllUsersRet: []chat.User{defaultUserRet}, 45 | PublicChannelsRet: []chat.Channel{defaultChannelRet}, 46 | NameRet: "Mock Adapter", 47 | MaxLengthRet: -1, 48 | BotUserRet: &chat.BaseUser{ 49 | UserID: id, 50 | UserName: r.Name(), 51 | UserIsBot: true, 52 | }, 53 | } 54 | }) 55 | } 56 | 57 | // MockChatAdapter provides an "empty" chat adapter which can be used to test 58 | // victor and/or victor handler functions. It stores all sent messages to an 59 | // exported array and allows certain function's returned values (GetUser, 60 | // IsPotentialUser, etc.) to be set. 61 | type MockChatAdapter struct { 62 | id string 63 | robot chat.Robot 64 | Sent, 65 | SentPublic, 66 | SentDirect []MockMessagePair 67 | NameRet string 68 | UserRet chat.User 69 | ChannelRet chat.Channel 70 | AllUsersRet []chat.User 71 | BotUserRet chat.User 72 | PublicChannelsRet []chat.Channel 73 | GeneralChannelRet chat.Channel 74 | IsPotentialUserRet bool 75 | IsPotentialChannelRet bool 76 | MaxLengthRet int 77 | } 78 | 79 | // Clear clears the contents of the "Sent" array. 80 | func (m *MockChatAdapter) Clear() { 81 | m.Sent = make([]MockMessagePair, 0, 10) 82 | m.SentPublic = make([]MockMessagePair, 0, 10) 83 | m.SentDirect = make([]MockMessagePair, 0, 10) 84 | } 85 | 86 | func (m *MockChatAdapter) MaxLength() int { 87 | return m.MaxLengthRet 88 | } 89 | 90 | func (m *MockChatAdapter) Name() string { 91 | return m.NameRet 92 | } 93 | 94 | // Receive mocks a message being received by the chat adapter. 95 | func (m *MockChatAdapter) Receive(mp chat.Message) { 96 | m.robot.Receive(mp) 97 | } 98 | 99 | // Run does nothing as the mockAdapter does not connect to anything. 100 | func (m *MockChatAdapter) Run() { 101 | return 102 | } 103 | 104 | // Send stores the given channelID and text to the exported array "Sent" as 105 | // a MockMessagePair. 106 | func (m *MockChatAdapter) Send(channelID, text string) { 107 | m.Sent = append(m.Sent, MockMessagePair{ 108 | text: text, 109 | channelID: channelID, 110 | isDirect: false, 111 | }) 112 | m.SentPublic = append(m.SentPublic, MockMessagePair{ 113 | text: text, 114 | channelID: channelID, 115 | isDirect: false, 116 | }) 117 | } 118 | 119 | // SendDirectMessage stores the given userID and text to the exported array 120 | // "Sent" as a MockMessagePair with the "IsDirect" flag set to true. 121 | func (m *MockChatAdapter) SendDirectMessage(userID, text string) { 122 | m.Sent = append(m.Sent, MockMessagePair{ 123 | text: text, 124 | userID: userID, 125 | isDirect: true, 126 | }) 127 | m.SentDirect = append(m.SentDirect, MockMessagePair{ 128 | text: text, 129 | userID: userID, 130 | isDirect: true, 131 | }) 132 | } 133 | 134 | // SendTyping does nothing. 135 | func (m *MockChatAdapter) SendTyping(channelID string) { 136 | return 137 | } 138 | 139 | // Stop does nothing. 140 | func (m *MockChatAdapter) Stop() { 141 | return 142 | } 143 | 144 | // ID returns the mockAdapter's ID. 145 | func (m *MockChatAdapter) ID() string { 146 | return m.id 147 | } 148 | 149 | // GetUser returns the mockAdapter's set "UserRet" property which can be 150 | // set to any chat.User instance (default value has full name "Fake User"). 151 | func (m *MockChatAdapter) GetUser(string) chat.User { 152 | return m.UserRet 153 | } 154 | 155 | func (m *MockChatAdapter) GetBot() chat.User { 156 | return m.BotUserRet 157 | } 158 | 159 | func (m *MockChatAdapter) GetChannel(string) chat.Channel { 160 | return m.ChannelRet 161 | } 162 | 163 | func (m *MockChatAdapter) GetAllUsers() []chat.User { 164 | return m.AllUsersRet 165 | } 166 | 167 | func (m *MockChatAdapter) GetPublicChannels() []chat.Channel { 168 | return m.PublicChannelsRet 169 | } 170 | 171 | func (m *MockChatAdapter) GetGeneralChannel() chat.Channel { 172 | return m.GeneralChannelRet 173 | } 174 | 175 | // IsPotentialUser returns the mockAdapter's set "IsPotentialUserRet" property 176 | // which has a default value of "true". 177 | func (m *MockChatAdapter) IsPotentialUser(string) bool { 178 | return m.IsPotentialUserRet 179 | } 180 | 181 | func (m *MockChatAdapter) IsPotentialChannel(string) bool { 182 | return m.IsPotentialChannelRet 183 | } 184 | 185 | // MockMessagePair is used to store messages that are sent by chat handlers to 186 | // the mockAdapter instance. 187 | type MockMessagePair struct { 188 | channelID, 189 | userID, 190 | text string 191 | isDirect bool 192 | } 193 | 194 | // ChannelID returns the id of the channel that the sent message was intended 195 | // for. If this is a direct message then this will not be set. 196 | func (mp *MockMessagePair) ChannelID() string { 197 | return mp.channelID 198 | } 199 | 200 | // Text returns the message's full text. 201 | func (mp *MockMessagePair) Text() string { 202 | return mp.text 203 | } 204 | 205 | // UserID returns the id of the user that the message was intended for. This 206 | // will not be sent unless it is a direct message. 207 | func (mp *MockMessagePair) UserID() string { 208 | return mp.userID 209 | } 210 | 211 | // IsDirect returns true if the message is direct and false otherwise. The 212 | // output of this can be used to determine whether ChannelID() or UserID are 213 | // relavent to this message. 214 | func (mp *MockMessagePair) IsDirect() bool { 215 | return mp.isDirect 216 | } 217 | -------------------------------------------------------------------------------- /dispatch_test.go: -------------------------------------------------------------------------------- 1 | package victor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/FogCreek/victor/pkg/chat" 7 | _ "github.com/FogCreek/victor/pkg/chat/mockAdapter" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const botName = "@testBot" 13 | 14 | // Returns a new *robot using the "mockAdapter" chat adapter and the bot name 15 | // set in the "botName" constant. 16 | func getMockBot() *robot { 17 | return New(Config{ 18 | Name: botName[1:], 19 | ChatAdapter: "mockAdapter", 20 | }) 21 | } 22 | 23 | // HandlerMock provides an easy way to construct a handler and assert how many 24 | // times it has been called by the message router and/or the fields that it is 25 | // called with. 26 | type HandlerMock struct { 27 | t *testing.T 28 | timesRun int 29 | expectedFields []string 30 | } 31 | 32 | // TimesRun returns the number of times that the function returned by this 33 | // HandlerMock's Func() method has been called. 34 | func (h *HandlerMock) TimesRun() int { 35 | return h.timesRun 36 | } 37 | 38 | // Func returns a function of type HandlerFunc which increments an internal 39 | // counter every time it is called. Multiple calls to Func() will return 40 | // different function instances but all will increment the same internal count. 41 | // 42 | // If the expected fields property is set to a non-nil value then a call to the 43 | // returned function will assert the expected and actual fields equality. 44 | func (h *HandlerMock) Func() HandlerFunc { 45 | return func(s State) { 46 | h.timesRun++ 47 | if h.expectedFields != nil { 48 | assert.Equal(h.t, h.expectedFields, s.Fields(), "Fields mismatch - fields incorrectly parsed.") 49 | } 50 | } 51 | } 52 | 53 | // HasRun asserts that this HandlerMock instance has been run the expected 54 | // number of times. This is equivalent to HasRunCustom but with a preset failed 55 | // message. 56 | func (h *HandlerMock) HasRun(expectedTimesRun int) { 57 | h.HasRunCustom(expectedTimesRun, "Count mismatch - handlers incorrectly called.") 58 | } 59 | 60 | // HasRunCustom asserts that this HandlerMock instance has been run the expected 61 | // number of times. The "failedMessage" parameter is the message that will be 62 | // shown if the assertion fails. 63 | func (h *HandlerMock) HasRunCustom(expectedTimesRun int, failedMessage string) { 64 | if expectedTimesRun != h.timesRun { 65 | assert.Fail(h.t, failedMessage) 66 | } 67 | // assert.Equal(h.t, expectedTimesRun, h.timesRun, failedMessage) 68 | } 69 | 70 | // ExpectFields sets up an assertion that the fields returned by the State 71 | // object upon a call to the handler are equal to the string slice given. 72 | // This overrides any previous expected fields. If this is set to nil then the 73 | // fields will not be checked on a call to the handler function. 74 | func (h *HandlerMock) ExpectFields(fields []string) { 75 | h.expectedFields = fields 76 | } 77 | 78 | func TestNewDispatch(t *testing.T) { 79 | bot := getMockBot() 80 | assert.Empty(t, bot.dispatch.commands, "A new bot should have no commands.") 81 | assert.Nil(t, bot.dispatch.defaultHandler, "Default handler should be nil.") 82 | assert.Empty(t, bot.commandNames, "No command names should be stored on creation.") 83 | assert.Empty(t, bot.patterns, "Patterns array should be empty on creation.") 84 | } 85 | 86 | func TestEnableHelp(t *testing.T) { 87 | bot := getMockBot() 88 | bot.dispatch.EnableHelpCommand() 89 | assert.Len(t, bot.dispatch.commands, 1, "Help handler should have been added once.") 90 | assert.Len(t, bot.dispatch.commandNames, 1, "Help handler should be in command names.") 91 | assert.Equal(t, helpCommandName, bot.dispatch.commandNames[0], "Help handler should be in command names.") 92 | for key := range bot.dispatch.commands { 93 | assert.Equal(t, helpCommandName, key, "Help handler command should be \"help\".") 94 | } 95 | } 96 | 97 | func TestCommandsGetter(t *testing.T) { 98 | bot := getMockBot() 99 | bot.dispatch.HandleCommand(&HandlerDoc{CmdName: "test"}) 100 | mapCopy := bot.dispatch.Commands() 101 | assert.Equal(t, mapCopy, bot.dispatch.commands, "Map copy should be equal") 102 | mapCopy["mock"] = nil 103 | assert.NotEqual(t, mapCopy, bot.dispatch.commands, "Map copy should be a copy of original commands map") 104 | } 105 | 106 | func TestHandleCommand(t *testing.T) { 107 | bot := getMockBot() 108 | handler := HandlerMock{t: t} 109 | bot.dispatch.HandleCommand(&HandlerDoc{ 110 | CmdHandler: handler.Func(), 111 | CmdName: "name", 112 | CmdUsage: []string{"", "1", "2"}, 113 | CmdDescription: "description", 114 | CmdIsHidden: true, 115 | }) 116 | handler.HasRunCustom(0, "Handler should not have been called on creation.") 117 | assert.Len(t, bot.dispatch.commands, 1, "Added command should be present in map.") 118 | assert.Len(t, bot.dispatch.commandNames, 1, "Added command should be in commandNames list.") 119 | // testify.assert.Contains doesn't support map keys right now https://github.com/stretchr/testify/pull/165 120 | actualHandlerFunc, exists := bot.dispatch.commands["name"] 121 | assert.True(t, exists, "Bot should contain new handler in commands map.") 122 | assert.Contains(t, bot.dispatch.commandNames, "name", "Bot should contain new handler in commandNames") 123 | assert.Equal(t, "name", actualHandlerFunc.Name(), "HandlerDoc name changed.") 124 | assert.Equal(t, []string{"", "1", "2"}, actualHandlerFunc.Usage(), "HandlerDoc usage changed.") 125 | assert.Equal(t, "description", actualHandlerFunc.Description(), "HandlerDoc description changed.") 126 | assert.True(t, actualHandlerFunc.IsHidden(), "HandlerDoc IsHidden property changed.") 127 | actualHandlerFunc.Handler().Handle(nil) 128 | handler.HasRunCustom(1, "Handler function should have increased count on call to Handle") 129 | } 130 | 131 | func TestProcessMessageCommand(t *testing.T) { 132 | bot := getMockBot() 133 | name0Handle := HandlerMock{t: t} 134 | name1Handle := HandlerMock{t: t} 135 | bot.dispatch.HandleCommand(&HandlerDoc{ 136 | CmdHandler: name0Handle.Func(), 137 | CmdName: "name0", 138 | }) 139 | bot.dispatch.HandleCommand(&HandlerDoc{ 140 | CmdHandler: name1Handle.Func(), 141 | CmdName: "name1", 142 | }) 143 | // by default will not be in a direct message unless specified otherwise 144 | // should not call a handler 145 | bot.dispatch.ProcessMessage(&chat.BaseMessage{MsgText: "name0"}) 146 | name0Handle.HasRunCustom(0, "Handler should not have been called yet.") 147 | name1Handle.HasRunCustom(0, "Handler should not have been called yet.") 148 | // should call "name0" handler 149 | bot.dispatch.ProcessMessage(&chat.BaseMessage{MsgText: "name0", MsgIsDirect: true}) 150 | name0Handle.HasRunCustom(1, "\"name0\" handler should have been called") 151 | name1Handle.HasRunCustom(0, "\"name1\" handler should not have been called") 152 | // should call "name0" handler 153 | bot.dispatch.ProcessMessage(&chat.BaseMessage{MsgText: botName + " name0"}) 154 | name0Handle.HasRun(2) 155 | name1Handle.HasRun(0) 156 | // should call "name1" handler 157 | bot.dispatch.ProcessMessage(&chat.BaseMessage{MsgText: botName + "name1 param"}) 158 | name0Handle.HasRun(2) 159 | name1Handle.HasRun(1) 160 | } 161 | 162 | func TestFieldsDirectly(t *testing.T) { 163 | var expectedOutput []string 164 | var input string 165 | 166 | expectedOutput = []string{} 167 | input = "" 168 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 169 | 170 | expectedOutput = []string{} 171 | input = " \t\t\n " 172 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 173 | 174 | expectedOutput = []string{"a"} 175 | input = "a" 176 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 177 | 178 | expectedOutput = []string{"a", "b"} 179 | input = "a b" 180 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 181 | 182 | expectedOutput = []string{"a", "b"} 183 | input = "a\t\t\n b" 184 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 185 | 186 | expectedOutput = []string{"a\t b"} 187 | input = "\"a\t b\"" 188 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 189 | 190 | input = "you're it's \"i'm who's\"" 191 | expectedOutput = []string{"you're", "it's", "i'm who's"} 192 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 193 | 194 | input = "\"test of\" \t\n unclosed\t \"quotes and\tspaces" 195 | expectedOutput = []string{"test of", "unclosed", "quotes and\tspaces"} 196 | assert.Equal(t, expectedOutput, parseFields(input), "Incorrect field parsing.") 197 | } 198 | 199 | func TestFieldsThroughBot(t *testing.T) { 200 | msg := &chat.BaseMessage{MsgIsDirect: true} 201 | bot := getMockBot() 202 | handler := HandlerMock{t: t} 203 | bot.dispatch.HandleCommand(&HandlerDoc{ 204 | CmdHandler: handler.Func(), 205 | CmdName: "test", 206 | }) 207 | 208 | handler.ExpectFields([]string{}) 209 | msg.MsgText = "test" 210 | bot.ProcessMessage(msg) 211 | handler.HasRun(1) 212 | 213 | handler.ExpectFields([]string{"test"}) 214 | msg.MsgText = "test \t\n test" 215 | bot.ProcessMessage(msg) 216 | handler.HasRun(2) 217 | 218 | handler.ExpectFields([]string{"this", "is", "a", "test"}) 219 | msg.MsgText = "test this is a test" 220 | bot.ProcessMessage(msg) 221 | handler.HasRun(3) 222 | 223 | handler.ExpectFields([]string{"this", " is a test"}) 224 | msg.MsgText = "test this \" is a test\"" 225 | bot.ProcessMessage(msg) 226 | handler.HasRun(4) 227 | } 228 | 229 | func TestFieldsDefaultHandler(t *testing.T) { 230 | msg := &chat.BaseMessage{MsgIsDirect: true} 231 | bot := getMockBot() 232 | handler := HandlerMock{t: t} 233 | bot.dispatch.SetDefaultHandler(handler.Func()) 234 | 235 | handler.ExpectFields([]string{}) 236 | msg.MsgText = "" 237 | bot.ProcessMessage(msg) 238 | handler.HasRun(1) 239 | 240 | handler.ExpectFields([]string{"test"}) 241 | msg.MsgText = "test" 242 | bot.ProcessMessage(msg) 243 | handler.HasRun(2) 244 | 245 | handler.ExpectFields([]string{"test", "this", "is", "a", "test"}) 246 | msg.MsgText = "test this is a test" 247 | bot.ProcessMessage(msg) 248 | handler.HasRun(3) 249 | 250 | handler.ExpectFields([]string{"test", "this", " is a test"}) 251 | msg.MsgText = "test this \" is a test\"" 252 | bot.ProcessMessage(msg) 253 | handler.HasRun(4) 254 | 255 | handler.ExpectFields([]string{"123"}) 256 | msg.MsgText = botName + " 123" 257 | bot.ProcessMessage(msg) 258 | handler.HasRun(5) 259 | } 260 | 261 | func TestDefaultHandler(t *testing.T) { 262 | bot := getMockBot() 263 | defaultHandle := HandlerMock{t: t} 264 | otherHandle := HandlerMock{t: t} 265 | bot.dispatch.HandleCommand(&HandlerDoc{ 266 | CmdHandler: otherHandle.Func(), 267 | CmdName: "test", 268 | }) 269 | bot.dispatch.SetDefaultHandler(defaultHandle.Func()) 270 | msg := &chat.BaseMessage{MsgIsDirect: true} 271 | // should call default handler 272 | bot.ProcessMessage(msg) 273 | defaultHandle.HasRun(1) 274 | otherHandle.HasRun(0) 275 | msg.MsgText = "test" 276 | // should not call default handler but should call other handler 277 | bot.ProcessMessage(msg) 278 | defaultHandle.HasRun(1) 279 | otherHandle.HasRun(1) 280 | msg.MsgText = "asdf" 281 | msg.MsgIsDirect = false 282 | // should not call any handlers 283 | bot.ProcessMessage(msg) 284 | defaultHandle.HasRun(1) 285 | otherHandle.HasRun(1) 286 | msg.MsgText = botName + " asdf" 287 | // should call default handler even though not direct 288 | bot.ProcessMessage(msg) 289 | defaultHandle.HasRun(2) 290 | otherHandle.HasRun(1) 291 | } 292 | 293 | func TestPatterns(t *testing.T) { 294 | bot := getMockBot() 295 | commandHandle := HandlerMock{t: t} 296 | patternHandle := HandlerMock{t: t} 297 | defaultHandle := HandlerMock{t: t} 298 | bot.HandleCommand(&HandlerDoc{ 299 | CmdHandler: commandHandle.Func(), 300 | CmdName: "pattern", 301 | }) 302 | // set up known pattern 303 | // case insensitive match for word "pattern" or "patterns" 304 | bot.HandlePattern("(?i)\\s*pattern[s]?\\s*", patternHandle.Func()) 305 | bot.SetDefaultHandler(defaultHandle.Func()) 306 | 307 | defaultHandle.HasRun(0) 308 | commandHandle.HasRun(0) 309 | patternHandle.HasRun(0) 310 | msg := &chat.BaseMessage{MsgIsDirect: false} 311 | msg.MsgText = "pattern" 312 | // should fire pattern and not cmd or defualt 313 | bot.ProcessMessage(msg) 314 | defaultHandle.HasRun(0) 315 | commandHandle.HasRun(0) 316 | patternHandle.HasRun(1) 317 | msg.MsgIsDirect = true 318 | // should fire on cmd and not pattern 319 | bot.ProcessMessage(msg) 320 | defaultHandle.HasRun(0) 321 | commandHandle.HasRun(1) 322 | patternHandle.HasRun(1) 323 | msg.MsgText = "patterns" 324 | // should fire default handler 325 | bot.ProcessMessage(msg) 326 | defaultHandle.HasRun(1) 327 | commandHandle.HasRun(1) 328 | patternHandle.HasRun(1) 329 | msg.MsgIsDirect = false 330 | // should fire pattern 331 | bot.ProcessMessage(msg) 332 | defaultHandle.HasRun(1) 333 | commandHandle.HasRun(1) 334 | patternHandle.HasRun(2) 335 | msg.MsgText = "test for the_PaTTeRNs_handler" 336 | // should still match the pattern 337 | bot.ProcessMessage(msg) 338 | defaultHandle.HasRun(1) 339 | commandHandle.HasRun(1) 340 | patternHandle.HasRun(3) 341 | } 342 | 343 | func TestCommandPatternsFiring(t *testing.T) { 344 | bot := getMockBot() 345 | patternHandle := HandlerMock{t: t} 346 | commandHandle := HandlerMock{t: t} 347 | defaultHandle := HandlerMock{t: t} 348 | // set up pattern command 349 | // matches "hank", "hanks", thank", and "thanks" case insensitively 350 | bot.HandleCommandPattern("(?i)[t]?hank[s]?", &HandlerDoc{ 351 | CmdHandler: patternHandle.Func(), 352 | CmdName: "pattern", 353 | }) 354 | bot.HandleCommand(&HandlerDoc{ 355 | CmdHandler: commandHandle.Func(), 356 | CmdName: "thanks", 357 | }) 358 | bot.SetDefaultHandler(defaultHandle.Func()) 359 | msg := &chat.BaseMessage{} 360 | 361 | msg.MsgIsDirect = false 362 | msg.MsgText = "thank you" 363 | // should not fire command pattern or default 364 | bot.ProcessMessage(msg) 365 | defaultHandle.HasRun(0) 366 | patternHandle.HasRun(0) 367 | commandHandle.HasRun(0) 368 | 369 | msg.MsgIsDirect = true 370 | msg.MsgText = "pattern" 371 | // should fire default handler 372 | bot.ProcessMessage(msg) 373 | defaultHandle.HasRun(1) 374 | patternHandle.HasRun(0) 375 | commandHandle.HasRun(0) 376 | 377 | msg.MsgText = "thanks" 378 | // should fire command and not pattern or default 379 | bot.ProcessMessage(msg) 380 | defaultHandle.HasRun(1) 381 | patternHandle.HasRun(0) 382 | commandHandle.HasRun(1) 383 | 384 | msg.MsgIsDirect = false 385 | msg.MsgText = botName + " thank you" 386 | // should fire command pattern 387 | bot.ProcessMessage(msg) 388 | defaultHandle.HasRun(1) 389 | patternHandle.HasRun(1) 390 | commandHandle.HasRun(1) 391 | 392 | msg.MsgText = botName + "than you" 393 | // should fire default handler 394 | bot.ProcessMessage(msg) 395 | defaultHandle.HasRun(2) 396 | patternHandle.HasRun(1) 397 | commandHandle.HasRun(1) 398 | 399 | msg.MsgIsDirect = true 400 | msg.MsgText = "ThAnk \tyou field1 field2" 401 | // should fire command pattern 402 | bot.ProcessMessage(msg) 403 | defaultHandle.HasRun(2) 404 | patternHandle.HasRun(2) 405 | commandHandle.HasRun(1) 406 | 407 | msg.MsgIsDirect = true 408 | msg.MsgText = "abcd thank you efg" 409 | // should fire default handler despite match in middle of string 410 | bot.ProcessMessage(msg) 411 | defaultHandle.HasRun(3) 412 | patternHandle.HasRun(2) 413 | commandHandle.HasRun(1) 414 | } 415 | 416 | func TestCommandPatternsFields(t *testing.T) { 417 | bot := getMockBot() 418 | handler := HandlerMock{t: t} 419 | // matches "hank", "hanks", thank", and "thanks" case insensitively 420 | bot.HandleCommandPattern("(?i)[t]?hank[s]?", &HandlerDoc{ 421 | CmdHandler: handler.Func(), 422 | CmdName: "pattern", 423 | }) 424 | msg := &chat.BaseMessage{MsgIsDirect: true} 425 | 426 | msg.MsgText = "thanks" 427 | handler.ExpectFields([]string{}) 428 | bot.ProcessMessage(msg) 429 | handler.HasRun(1) 430 | 431 | msg.MsgText = "thanks\t\t\n " 432 | handler.ExpectFields([]string{}) 433 | bot.ProcessMessage(msg) 434 | handler.HasRun(2) 435 | 436 | msg.MsgText = "thank s a bunch" 437 | handler.ExpectFields([]string{"s", "a", "bunch"}) 438 | bot.ProcessMessage(msg) 439 | handler.HasRun(3) 440 | 441 | msg.MsgText = "ThAnkyou for \"every thing\"" 442 | handler.ExpectFields([]string{"for", "every thing"}) 443 | bot.ProcessMessage(msg) 444 | handler.HasRun(4) 445 | } 446 | -------------------------------------------------------------------------------- /pkg/chat/slackRealtime/slackRealtime.go: -------------------------------------------------------------------------------- 1 | package slackRealtime 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/FogCreek/slack" 12 | "github.com/FogCreek/victor/pkg/chat" 13 | "github.com/FogCreek/victor/pkg/events" 14 | "github.com/FogCreek/victor/pkg/events/definedEvents" 15 | ) 16 | 17 | const ( 18 | // TokenLength is the expected length of a Slack API auth token. 19 | TokenLength = 40 20 | 21 | // AdapterName is the Slack Websocket's registered adapter name for the 22 | // victor framework. 23 | AdapterName = "slackRealtime" 24 | 25 | // archiveURLFormat defines a printf-style format string for building 26 | // archive links centered around a message using the slack instance's 27 | // team name, the channel name, and the message's timestamp. 28 | archiveURLFormat = "https://%s.slack.com/archives/%s/p%s" 29 | ) 30 | 31 | var ( 32 | // Match "<@Userid>" and "<@UserID|fullname>" 33 | userIDRegexp = regexp.MustCompile(`^<@(U[[:alnum:]]+)(?:(?:|\S+)?>)`) 34 | 35 | // Match "<#ChannelID>" and "<#ChannelID|name>" 36 | channelIDRegexp = regexp.MustCompile(`^<#(C[[:alnum:]]+)(?:(?:|\S+)?>)`) 37 | 38 | // Should match all formatted slack inputs and have a capturing group of 39 | // the desired value from the formatted group. 40 | formattingRegexp = regexp.MustCompile(`<(?:mailto\:)?([^\|>]+)\|?[^>]*>`) 41 | 42 | // If a message part starts with any of these prefixes (case sensitive) 43 | // then it should not be unformatted by "unescapeMessage". 44 | unformattedPrefixes = []string{"@U", "#C", "!"} 45 | ) 46 | 47 | // channelGroupInfo is used instead of the slack library's Channel struct since we 48 | // are trying to consider channels and groups to be roughly the same while it 49 | // considers them seperate and provides no way to consolidate them on its own. 50 | // 51 | // This also allows us to throw our information that we don't care about (members, etc.). 52 | type channelGroupInfo struct { 53 | Name string 54 | ID string 55 | IsDM bool 56 | IsChannel bool 57 | IsGeneral bool 58 | UserID string 59 | // UserID is only stored for IM/DM's so we can then send a user a DM as a 60 | // response if needed 61 | } 62 | 63 | // init registers SlackAdapter to the victor chat framework. 64 | func init() { 65 | chat.Register(AdapterName, func(r chat.Robot) chat.Adapter { 66 | config, configSet := r.AdapterConfig() 67 | if !configSet { 68 | log.Println("A configuration struct implementing the SlackConfig interface must be set.") 69 | os.Exit(1) 70 | } 71 | sConfig, ok := config.(Config) 72 | if !ok { 73 | log.Println("The bot's config must implement the SlackConfig interface.") 74 | os.Exit(1) 75 | } 76 | return &SlackAdapter{ 77 | robot: r, 78 | chReceiver: make(chan slack.SlackEvent), 79 | token: sConfig.Token(), 80 | channelInfo: make(map[string]channelGroupInfo), 81 | directMessageID: make(map[string]string), 82 | userInfo: make(map[string]slack.User), 83 | mutex: &sync.RWMutex{}, 84 | botUser: &chat.BaseUser{ 85 | UserName: "unknown", // We don't know our username until the adapter is started 86 | UserIsBot: true, 87 | }, 88 | } 89 | }) 90 | } 91 | 92 | // Config provides the slack adapter with the necessary 93 | // information to open a websocket connection with the slack Real time API. 94 | type Config interface { 95 | Token() string 96 | } 97 | 98 | // Config implements the SlackRealtimeConfig interface to provide a slack 99 | // adapter with the information it needs to authenticate with slack. 100 | type configImpl struct { 101 | token string 102 | } 103 | 104 | // NewConfig returns a new slack configuration instance using the given token. 105 | func NewConfig(token string) configImpl { 106 | return configImpl{token: token} 107 | } 108 | 109 | // Token returns the slack token. 110 | func (c configImpl) Token() string { 111 | return c.token 112 | } 113 | 114 | // SlackAdapter holds all information needed by the adapter to send/receive messages. 115 | type SlackAdapter struct { 116 | robot chat.Robot 117 | token string 118 | rtm *slack.RTM 119 | chReceiver chan slack.SlackEvent 120 | channelInfo map[string]channelGroupInfo 121 | directMessageID map[string]string 122 | userInfo map[string]slack.User 123 | mutex *sync.RWMutex 124 | botUser chat.User 125 | formattedSlackID, 126 | domain, 127 | teamName string 128 | } 129 | 130 | func (adapter *SlackAdapter) MaxLength() int { 131 | return slack.MaxMessageTextLength 132 | } 133 | 134 | // GetUser will parse the given user ID string and then return the user's 135 | // information as provided by the slack API. This will first try to get the 136 | // user's information from a local cache and then will perform a slack API 137 | // call if the user's information is not cached. Returns nil if the user does 138 | // not exist or if an error occurrs during the slack API call. 139 | func (adapter *SlackAdapter) GetUser(userIDStr string) chat.User { 140 | if !adapter.IsPotentialUser(userIDStr) { 141 | log.Printf("%s is not a potential user", userIDStr) 142 | return nil 143 | } 144 | userID := normalizeID(userIDStr, userIDRegexp) 145 | userObj, err := adapter.getUserFromSlack(userID) 146 | if err != nil { 147 | log.Println("Error getting user:", err.Error()) 148 | return nil 149 | } 150 | return &chat.BaseUser{ 151 | UserID: userObj.Id, 152 | UserName: userObj.Name, 153 | UserEmail: userObj.Profile.Email, 154 | UserIsBot: userObj.IsBot, 155 | } 156 | } 157 | 158 | func (adapter *SlackAdapter) GetChannel(channelIDStr string) chat.Channel { 159 | if !adapter.IsPotentialChannel(channelIDStr) { 160 | log.Printf("%s is not a potential channel", channelIDStr) 161 | return nil 162 | } 163 | channelID := normalizeID(channelIDStr, channelIDRegexp) 164 | channelObj := adapter.getChannelFromSlack(channelID) 165 | if channelObj.Name == "Unrecognized" { 166 | return nil 167 | } 168 | return &chat.BaseChannel{ 169 | ChannelID: channelObj.ID, 170 | ChannelName: channelObj.Name, 171 | } 172 | } 173 | 174 | // normalizeID returns an ID without the extra formatting that slack might add. 175 | // 176 | // This returns the first captured field of the first submatch using the given 177 | // precompiled regexp. If no matches are found or no captured groups are 178 | // defined then this returns the input text unchanged. 179 | func normalizeID(id string, exp *regexp.Regexp) string { 180 | idArr := exp.FindAllStringSubmatch(id, 1) 181 | if len(idArr) == 0 || len(idArr[0]) < 2 { 182 | return id 183 | } 184 | return idArr[0][1] 185 | } 186 | 187 | // GetAllUsers returns a slice of all user objects that are known to the 188 | // chatbot. This does not perform a slack API call as all users should be 189 | // stored locally and any new users will be added upon a team join event. 190 | func (adapter *SlackAdapter) GetAllUsers() []chat.User { 191 | adapter.mutex.RLock() 192 | defer adapter.mutex.RUnlock() 193 | var users []chat.User 194 | for _, u := range adapter.userInfo { 195 | users = append(users, &chat.BaseUser{ 196 | UserID: u.Id, 197 | UserName: u.Name, 198 | UserEmail: u.Profile.Email, 199 | UserIsBot: u.IsBot, 200 | }) 201 | } 202 | return users 203 | } 204 | 205 | func (adapter *SlackAdapter) GetBot() chat.User { 206 | adapter.mutex.RLock() 207 | defer adapter.mutex.RUnlock() 208 | return adapter.botUser 209 | } 210 | 211 | // GetPublicChannels returns a slice of all channels that are known to the 212 | // chatbot. 213 | func (adapter *SlackAdapter) GetPublicChannels() []chat.Channel { 214 | adapter.mutex.RLock() 215 | defer adapter.mutex.RUnlock() 216 | var channels []chat.Channel 217 | for _, c := range adapter.channelInfo { 218 | if c.IsChannel { 219 | channels = append(channels, &chat.BaseChannel{ 220 | ChannelID: c.ID, 221 | ChannelName: c.Name, 222 | }) 223 | } 224 | } 225 | return channels 226 | } 227 | 228 | // GetGeneralChannel returns the known slack channel that is considered to be 229 | // "general" by the slack API. If none is set or the adapter is not a member 230 | // of a general channel then nil is returned. 231 | func (adapter *SlackAdapter) GetGeneralChannel() chat.Channel { 232 | adapter.mutex.RLock() 233 | defer adapter.mutex.RUnlock() 234 | for _, c := range adapter.channelInfo { 235 | if c.IsChannel && c.IsGeneral { 236 | return &chat.BaseChannel{ 237 | ChannelID: c.ID, 238 | ChannelName: c.Name, 239 | } 240 | } 241 | } 242 | return nil 243 | } 244 | 245 | // IsPotentialUser checks if a given string is potentially referring to a slack 246 | // user. Strings given to this function should be trimmed of leading whitespace 247 | // as it does not account for that (it is meant to be used with the fields 248 | // method on the frameworks calls to handlers which are trimmed). 249 | func (adapter *SlackAdapter) IsPotentialUser(userString string) bool { 250 | return userIDRegexp.MatchString(userString) 251 | } 252 | 253 | // IsPotentialChannel checks if a given string is potentially referring to a 254 | // slack channel. Strings given to this function should be trimmed of leading 255 | // whitespace as it does not account for that (it is meant to be used with the 256 | // fields method on the frameworks calls to handlers which are trimmed). 257 | func (adapter *SlackAdapter) IsPotentialChannel(channelString string) bool { 258 | return channelIDRegexp.MatchString(channelString) 259 | } 260 | 261 | // Run starts the adapter and begins to listen for new messages to send/receive. 262 | // At the moment this will crash the program and print the error messages to a 263 | // log if the connection fails. 264 | func (adapter *SlackAdapter) Run() { 265 | client := slack.New(adapter.token) 266 | client.SetDebug(false) 267 | adapter.rtm = client.NewRTM() 268 | go adapter.monitorEvents() 269 | go adapter.rtm.ManageConnection() 270 | } 271 | 272 | func (adapter *SlackAdapter) Name() string { 273 | adapter.mutex.RLock() 274 | defer adapter.mutex.RUnlock() 275 | return adapter.teamName 276 | } 277 | 278 | func (adapter *SlackAdapter) initAdapterInfo(info *slack.Info) { 279 | adapter.mutex.Lock() 280 | defer adapter.robot.RefreshUserName() 281 | defer adapter.mutex.Unlock() 282 | adapter.formattedSlackID = fmt.Sprintf("<@%s>", info.User.Id) 283 | adapter.botUser = &chat.BaseUser{ 284 | UserName: info.User.Name, 285 | UserID: info.User.Id, 286 | UserIsBot: true, 287 | } 288 | adapter.domain = info.Team.Domain 289 | adapter.teamName = info.Team.Name 290 | for _, channel := range info.Channels { 291 | if !channel.IsMember { 292 | continue 293 | } 294 | adapter.channelInfo[channel.Id] = channelGroupInfo{ 295 | ID: channel.Id, 296 | Name: channel.Name, 297 | IsChannel: true, 298 | IsDM: false, 299 | IsGeneral: channel.IsGeneral, 300 | } 301 | } 302 | for _, group := range info.Groups { 303 | adapter.channelInfo[group.Id] = channelGroupInfo{ 304 | ID: group.Id, 305 | Name: group.Name, 306 | IsChannel: false, 307 | IsDM: false, 308 | } 309 | } 310 | for _, im := range info.IMs { 311 | adapter.channelInfo[im.Id] = channelGroupInfo{ 312 | ID: im.Id, 313 | Name: fmt.Sprintf("DM %s", im.Id), 314 | IsChannel: false, 315 | IsDM: true, 316 | UserID: im.UserId, 317 | } 318 | adapter.directMessageID[im.UserId] = im.Id 319 | } 320 | for _, user := range info.Users { 321 | if user.Deleted { 322 | continue 323 | } 324 | adapter.userInfo[user.Id] = user 325 | } 326 | } 327 | 328 | // Stop stops the adapter. 329 | func (adapter *SlackAdapter) Stop() { 330 | adapter.rtm.Disconnect() 331 | } 332 | 333 | // ID returns a unique ID for this adapter. At the moment this just returns 334 | // the slack instance token but could be modified to return a uuid using a 335 | // package such as https://godoc.org/code.google.com/p/go-uuid/uuid 336 | func (adapter *SlackAdapter) ID() string { 337 | return adapter.token 338 | } 339 | 340 | func (adapter *SlackAdapter) getUserFromSlack(userID string) (*slack.User, error) { 341 | adapter.mutex.RLock() 342 | // try to get the stored user info 343 | user, exists := adapter.userInfo[userID] 344 | adapter.mutex.RUnlock() 345 | // if it hasn't been stored then perform a slack API call to get it and 346 | // store it 347 | if !exists { 348 | adapter.mutex.Lock() 349 | defer adapter.mutex.Unlock() 350 | user, err := adapter.rtm.Client.GetUserInfo(userID) 351 | if err != nil { 352 | log.Println(err.Error()) 353 | return nil, err 354 | } 355 | // try to encode it as a json string for storage 356 | adapter.userInfo[user.Id] = *user 357 | return user, nil 358 | } 359 | 360 | return &user, nil 361 | } 362 | 363 | func (adapter *SlackAdapter) getChannelFromSlack(channelID string) channelGroupInfo { 364 | adapter.mutex.RLock() 365 | channel, exists := adapter.channelInfo[channelID] 366 | adapter.mutex.RUnlock() 367 | if exists { 368 | return channel 369 | } 370 | adapter.mutex.Lock() 371 | defer adapter.mutex.Unlock() 372 | channelObj, err := adapter.rtm.GetChannelInfo(channelID) 373 | if err != nil { 374 | log.Printf("Unrecognized channel with ID %s", channelID) 375 | return channelGroupInfo{ 376 | Name: "Unrecognized", 377 | ID: channelID, 378 | } 379 | } 380 | info := channelGroupInfo{ 381 | ID: channelObj.Id, 382 | Name: channelObj.Name, 383 | IsChannel: true, 384 | IsGeneral: channelObj.IsGeneral, 385 | } 386 | adapter.channelInfo[channelObj.Id] = info 387 | return info 388 | } 389 | 390 | func (adapter *SlackAdapter) handleMessage(event *slack.MessageEvent) { 391 | if len(event.SubType) > 0 { 392 | return 393 | } 394 | user, _ := adapter.getUserFromSlack(event.UserId) 395 | channel := adapter.getChannelFromSlack(event.ChannelId) 396 | // TODO use error 397 | if user != nil { 398 | // ignore any messages that are sent by any bot 399 | if user.IsBot { 400 | return 401 | } 402 | messageText := adapter.unescapeMessage(event.Text) 403 | var archiveLink string 404 | if !channel.IsDM { 405 | archiveLink = adapter.getArchiveLink(channel.Name, event.Timestamp) 406 | } else { 407 | archiveLink = "No archive link for Direct Messages" 408 | } 409 | msg := chat.BaseMessage{ 410 | MsgUser: &chat.BaseUser{ 411 | UserID: user.Id, 412 | UserName: user.Name, 413 | UserEmail: user.Profile.Email, 414 | }, 415 | MsgChannel: &chat.BaseChannel{ 416 | ChannelID: channel.ID, 417 | ChannelName: channel.Name, 418 | }, 419 | MsgText: messageText, 420 | MsgIsDirect: channel.IsDM, 421 | MsgTimestamp: strings.SplitN(event.Timestamp, ".", 2)[0], 422 | MsgArchiveLink: archiveLink, 423 | } 424 | adapter.robot.Receive(&msg) 425 | } 426 | } 427 | 428 | func (adapter *SlackAdapter) getArchiveLink(channelName, timestamp string) string { 429 | adapter.mutex.RLock() 430 | defer adapter.mutex.RUnlock() 431 | return fmt.Sprintf(archiveURLFormat, adapter.domain, channelName, strings.Replace(timestamp, ".", "", 1)) 432 | } 433 | 434 | // Fix formatting on incoming slack messages. 435 | // 436 | // This will also check if the message starts with the bot's user id. If it 437 | // does then it replaces it with the text version of the bot's name 438 | // (ex: "@victor") so the victor dispatch can recognize it as being directed 439 | // at the bot. 440 | func (adapter *SlackAdapter) unescapeMessage(msg string) string { 441 | // special case for starting with the bot's name 442 | // could replace all instances of bot's name but we only care about the 443 | // first one and all subsequent occurrences will be in the same format 444 | // as other user names. 445 | if strings.HasPrefix(msg, adapter.formattedSlackID) { 446 | msg = "@" + adapter.robot.Name() + msg[len(adapter.formattedSlackID):] 447 | } 448 | // find all formatted parts of the message 449 | matches := formattingRegexp.FindAllStringSubmatch(msg, -1) 450 | for _, match := range matches { 451 | if shouldUnformat(match[1]) { 452 | // replace the full formatted string part with the captured value 453 | // from the "formattingRegexp" regex 454 | msg = strings.Replace(msg, match[0], match[1], 1) 455 | } 456 | } 457 | return msg 458 | } 459 | 460 | // shouldUnformat checks if a given formatted string from slack should be 461 | // unformatted (remove brackets and optional pipe with name). This uses the 462 | // "unformattedPrefixes" array and checks if the given string starts with one 463 | // of those defined prefixes. If it does, then it should not be unformatted and 464 | // this will return false. Otherwise this will return true but not perform the 465 | // unformatting. 466 | func shouldUnformat(part string) bool { 467 | for _, s := range unformattedPrefixes { 468 | if strings.HasPrefix(part, s) { 469 | return false 470 | } 471 | } 472 | return true 473 | } 474 | 475 | // monitorEvents handles incoming events and filters them to only worry about 476 | // incoming messages. 477 | func (adapter *SlackAdapter) monitorEvents() { 478 | errorChannel := adapter.robot.ChatErrors() 479 | eventChannel := adapter.robot.ChatEvents() 480 | for { 481 | event := <-adapter.rtm.IncomingEvents 482 | 483 | switch e := event.Data.(type) { 484 | case *slack.InvalidAuthEvent: 485 | errorChannel <- &definedEvents.InvalidAuth{} 486 | case *slack.ConnectingEvent: 487 | eventChannel <- &definedEvents.ConnectingEvent{} 488 | case *slack.ConnectedEvent: 489 | go adapter.initAdapterInfo(e.Info) 490 | eventChannel <- &definedEvents.ConnectedEvent{} 491 | case *slack.SlackWSError: 492 | errorChannel <- &events.BaseError{ 493 | ErrorObj: e, 494 | } 495 | case *slack.DisconnectedEvent: 496 | errorChannel <- &definedEvents.Disconnect{ 497 | Intentional: e.Intentional, 498 | } 499 | case *slack.MessageEvent: 500 | go adapter.handleMessage(e) 501 | case *slack.ChannelJoinedEvent: 502 | go adapter.joinedChannel(e.Channel, true) 503 | case *slack.GroupJoinedEvent: 504 | go adapter.joinedChannel(e.Channel, false) 505 | case *slack.IMCreatedEvent: 506 | go adapter.joinedIM(e) 507 | case *slack.ChannelLeftEvent: 508 | go adapter.leftChannel(e.ChannelId, true) 509 | case *slack.GroupLeftEvent: 510 | go adapter.leftChannel(e.ChannelId, false) 511 | case *slack.IMCloseEvent: 512 | go adapter.leftIM(e) 513 | case *slack.TeamDomainChangeEvent: 514 | go adapter.domainChanged(e) 515 | case *slack.TeamRenameEvent: 516 | go adapter.teamNameChanged(e) 517 | case *slack.UserChangeEvent: 518 | go adapter.userChanged(e.User) 519 | case *slack.TeamJoinEvent: 520 | go adapter.userChanged(*e.User) 521 | case *slack.ChannelRenameEvent: 522 | go adapter.channelRenamed(e.Channel) 523 | case *slack.UnmarshallingErrorEvent: 524 | errorChannel <- &events.BaseError{ 525 | ErrorObj: e.ErrorObj, 526 | } 527 | case *slack.OutgoingErrorEvent: 528 | errorChannel <- &events.BaseError{ 529 | ErrorObj: e.ErrorObj, 530 | } 531 | case *slack.MessageTooLongEvent: 532 | errorChannel <- &definedEvents.MessageTooLong{ 533 | MaxLength: e.MaxLength, 534 | Text: e.Message.Text, 535 | ChannelID: e.Message.ChannelId, 536 | } 537 | } 538 | } 539 | } 540 | 541 | func (adapter *SlackAdapter) channelRenamed(channel slack.ChannelRenameInfo) { 542 | adapter.mutex.Lock() 543 | defer adapter.mutex.Unlock() 544 | chatChannel := &chat.BaseChannel{ 545 | ChannelID: channel.Id, 546 | ChannelName: channel.Name, 547 | } 548 | if oldChannel, exists := adapter.channelInfo[channel.Id]; exists { 549 | adapter.robot.ChatEvents() <- &definedEvents.ChannelChangedEvent{ 550 | OldName: oldChannel.Name, 551 | Channel: chatChannel, 552 | } 553 | oldChannel.Name = channel.Name 554 | adapter.channelInfo[channel.Id] = oldChannel 555 | } 556 | } 557 | 558 | func (adapter *SlackAdapter) userChanged(user slack.User) { 559 | if user.IsBot { 560 | return 561 | } 562 | adapter.mutex.Lock() 563 | defer adapter.mutex.Unlock() 564 | chatUser := &chat.BaseUser{ 565 | UserID: user.Id, 566 | UserName: user.Name, 567 | UserEmail: user.Profile.Email, 568 | UserIsBot: user.IsBot, 569 | } 570 | if oldUser, exists := adapter.userInfo[user.Id]; exists { 571 | event := &definedEvents.UserChangedEvent{User: chatUser} 572 | changed := false 573 | if oldUser.Name != user.Name { 574 | event.OldName = oldUser.Name 575 | changed = true 576 | } 577 | if oldUser.Profile.Email != user.Profile.Email { 578 | event.OldEmailAddress = oldUser.Profile.Email 579 | changed = true 580 | } 581 | if changed { 582 | adapter.robot.ChatEvents() <- event 583 | } 584 | } else { 585 | adapter.robot.ChatEvents() <- &definedEvents.UserEvent{ 586 | User: chatUser, 587 | WasRemoved: false, 588 | } 589 | } 590 | adapter.userInfo[user.Id] = user 591 | } 592 | 593 | func (adapter *SlackAdapter) domainChanged(event *slack.TeamDomainChangeEvent) { 594 | adapter.mutex.Lock() 595 | defer adapter.mutex.Unlock() 596 | adapter.domain = event.Domain 597 | } 598 | 599 | func (adapter *SlackAdapter) teamNameChanged(event *slack.TeamRenameEvent) { 600 | adapter.mutex.Lock() 601 | defer adapter.mutex.Unlock() 602 | adapter.teamName = event.Name 603 | } 604 | 605 | func (adapter *SlackAdapter) joinedChannel(channel slack.Channel, isChannel bool) { 606 | adapter.mutex.Lock() 607 | defer adapter.mutex.Unlock() 608 | adapter.channelInfo[channel.Id] = channelGroupInfo{ 609 | Name: channel.Name, 610 | ID: channel.Id, 611 | IsChannel: isChannel, 612 | IsGeneral: channel.IsGeneral, 613 | } 614 | if isChannel { 615 | adapter.robot.ChatEvents() <- &definedEvents.ChannelEvent{ 616 | Channel: &chat.BaseChannel{ 617 | ChannelName: channel.Name, 618 | ChannelID: channel.Id, 619 | }, 620 | WasRemoved: false, 621 | } 622 | } 623 | } 624 | 625 | func (adapter *SlackAdapter) joinedIM(event *slack.IMCreatedEvent) { 626 | adapter.mutex.Lock() 627 | defer adapter.mutex.Unlock() 628 | adapter.channelInfo[event.Channel.Id] = channelGroupInfo{ 629 | Name: fmt.Sprintf("DM %s", event.Channel.Id), 630 | ID: event.Channel.Id, 631 | IsDM: true, 632 | UserID: event.UserId, 633 | } 634 | adapter.directMessageID[event.UserId] = event.Channel.Id 635 | } 636 | 637 | func (adapter *SlackAdapter) leftIM(event *slack.IMCloseEvent) { 638 | adapter.leftChannel(event.ChannelId, false) 639 | delete(adapter.directMessageID, event.UserId) 640 | } 641 | 642 | func (adapter *SlackAdapter) leftChannel(channelID string, isChannel bool) { 643 | adapter.mutex.Lock() 644 | defer adapter.mutex.Unlock() 645 | channelName := adapter.channelInfo[channelID].Name 646 | delete(adapter.channelInfo, channelID) 647 | if isChannel { 648 | adapter.robot.ChatEvents() <- &definedEvents.ChannelEvent{ 649 | Channel: &chat.BaseChannel{ 650 | ChannelName: channelName, 651 | ChannelID: channelID, 652 | }, 653 | WasRemoved: true, 654 | } 655 | } 656 | } 657 | 658 | // Send sends a message to the given slack channel. 659 | func (adapter *SlackAdapter) Send(channelID, msg string) { 660 | msgObj := adapter.rtm.NewOutgoingMessage(msg, channelID) 661 | adapter.rtm.SendMessage(msgObj) 662 | } 663 | 664 | // SendDirectMessage sends the given message to the given user in a direct 665 | // (private) message. 666 | func (adapter *SlackAdapter) SendDirectMessage(userID, msg string) { 667 | channelID, err := adapter.getDirectMessageID(userID) 668 | if err != nil { 669 | log.Printf("Error getting direct message channel ID for user \"%s\": %s", userID, err.Error()) 670 | return 671 | } 672 | adapter.Send(channelID, msg) 673 | } 674 | 675 | func (adapter *SlackAdapter) SendTyping(channelID string) { 676 | adapter.rtm.SendMessage(&slack.OutgoingMessage{Type: "typing", ChannelId: channelID}) 677 | } 678 | 679 | func (adapter *SlackAdapter) getDirectMessageID(userID string) (string, error) { 680 | adapter.mutex.RLock() 681 | channel, exists := adapter.channelInfo[userID] 682 | adapter.mutex.RUnlock() 683 | if !exists { 684 | _, _, channelID, err := adapter.rtm.Client.OpenIMChannel(userID) 685 | adapter.mutex.Lock() 686 | adapter.channelInfo[channelID] = channelGroupInfo{ 687 | ID: channelID, 688 | Name: fmt.Sprintf("DM %s", channelID), 689 | IsChannel: false, 690 | IsDM: true, 691 | UserID: userID, 692 | } 693 | adapter.directMessageID[userID] = channelID 694 | adapter.mutex.Unlock() 695 | return channelID, err 696 | } 697 | return channel.ID, nil 698 | } 699 | -------------------------------------------------------------------------------- /dispatch.go: -------------------------------------------------------------------------------- 1 | package victor 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | "sync" 11 | "unicode" 12 | 13 | "github.com/FogCreek/victor/pkg/chat" 14 | ) 15 | 16 | // Printf style format for a bot's name regular expression 17 | const botNameRegexFormat = `^(?i)^@?%s\s*[:,]?\s*` 18 | 19 | // Name of default "help" command that is added on a call to 20 | // *dispatch.EnableHelpCommand(). 21 | const helpCommandName = "help" 22 | 23 | // HandlerDocPair provides a common interface for command handlers to be added 24 | // to a victor Robot along with their name, description, and usage. This allows 25 | // for a "help" handler to be easily written. 26 | type HandlerDocPair interface { 27 | Handler() HandlerFunc 28 | Name() string 29 | IsRegexpCommand() bool 30 | Regexp() *regexp.Regexp 31 | Description() string 32 | Usage() []string 33 | IsHidden() bool 34 | AliasNames() []string 35 | AddAliasName(string) bool 36 | } 37 | 38 | // HandlerDoc provides a base implementation of the HandlerDocPair interface. 39 | type HandlerDoc struct { 40 | CmdHandler HandlerFunc 41 | CmdName string 42 | CmdDescription string 43 | CmdUsage []string 44 | CmdIsHidden bool 45 | cmdRegexp *regexp.Regexp 46 | cmdAliasNames []string 47 | } 48 | 49 | // IsHidden returns true if this command should be hidden from the help list of 50 | // commands. It will still be "visible" if accessed with help by name. 51 | func (d *HandlerDoc) IsHidden() bool { 52 | return d.CmdIsHidden 53 | } 54 | 55 | // Handler returns the HandlerFunc. 56 | func (d *HandlerDoc) Handler() HandlerFunc { 57 | return d.CmdHandler 58 | } 59 | 60 | // Name returns the handler's set command name. This is not guarenteed to be 61 | // normalized (all lower case). 62 | func (d *HandlerDoc) Name() string { 63 | return d.CmdName 64 | } 65 | 66 | // Regexp returns the handler's set regexp. This should be nil for a normal 67 | // command and not nil for a pattern command. 68 | func (d *HandlerDoc) Regexp() *regexp.Regexp { 69 | return d.cmdRegexp 70 | } 71 | 72 | // IsRegexpCommand returns true if this command has a set regular expression 73 | // or false if it's name should be used to match input to its handler. 74 | func (d *HandlerDoc) IsRegexpCommand() bool { 75 | return d.cmdRegexp != nil 76 | } 77 | 78 | // Description returns the command's set description. 79 | func (d *HandlerDoc) Description() string { 80 | return d.CmdDescription 81 | } 82 | 83 | // Usage returns an array of acceptable usages for this command. 84 | // The usages should not have the command's name in them in order to work 85 | // with the default help handler. 86 | func (d *HandlerDoc) Usage() []string { 87 | return d.CmdUsage 88 | } 89 | 90 | // AliasNames returns a sorted copy of the internal alias names slice. The 91 | // returned slice is safe to modify and will never be nil although it could be 92 | // a zero-length slice. 93 | func (d *HandlerDoc) AliasNames() []string { 94 | aliasNamesCopy := make([]string, len(d.cmdAliasNames)) 95 | copy(aliasNamesCopy, d.cmdAliasNames) 96 | return aliasNamesCopy 97 | } 98 | 99 | // AddAliasName adds a given alias name in sorted order to the internal alias 100 | // names slice. This does not actually affect the dispatch and is only used 101 | // for help text and to determine if an alias name has already been added. 102 | // 103 | // This returns true if the alias name was added and false if the given alias 104 | // name is already set. This is case sensitive. 105 | func (d *HandlerDoc) AddAliasName(aliasName string) bool { 106 | // binary search through sorted array to see if the alias name alraedy 107 | // exists 108 | pos := sort.SearchStrings(d.cmdAliasNames, aliasName) 109 | if len(d.cmdAliasNames) > 0 && pos < len(d.cmdAliasNames) && d.cmdAliasNames[pos] == aliasName { 110 | return false 111 | } 112 | d.cmdAliasNames = appendInOrderWithoutRepeats(d.cmdAliasNames, aliasName) 113 | return true 114 | } 115 | 116 | // Set up base default help handler. Before use a copy msut be made and the 117 | // CmdHandler property must be set. 118 | var defaultHelpHandlerDoc = HandlerDoc{ 119 | CmdName: helpCommandName, 120 | CmdDescription: "View list of commands and their usage.", 121 | CmdUsage: []string{"", "`command name`"}, 122 | } 123 | 124 | // HandlerRegExpPair provides an interface for a handler as well as the regular 125 | // expression which a message should match in order to pass control onto the 126 | // handler 127 | type HandlerRegExpPair interface { 128 | Exp() *regexp.Regexp 129 | Handler() HandlerFunc 130 | } 131 | 132 | type handlerPair struct { 133 | exp *regexp.Regexp 134 | handle HandlerFunc 135 | } 136 | 137 | func (pair *handlerPair) Exp() *regexp.Regexp { 138 | return pair.exp 139 | } 140 | 141 | func (pair *handlerPair) Handler() HandlerFunc { 142 | return pair.handle 143 | } 144 | 145 | type dispatch struct { 146 | robot Robot 147 | defaultHandler HandlerFunc 148 | commands map[string]HandlerDocPair 149 | regexpCommands []HandlerDocPair 150 | commandNames []string 151 | patterns []HandlerRegExpPair 152 | botNameRegex *regexp.Regexp 153 | handlerMutex *sync.RWMutex 154 | } 155 | 156 | // newDispatch returns a new *dispatch instance which matches all message 157 | // routing methods specified in the victor.Robot interface. 158 | func newDispatch(bot Robot) *dispatch { 159 | return &dispatch{ 160 | robot: bot, 161 | defaultHandler: nil, 162 | commands: make(map[string]HandlerDocPair), 163 | botNameRegex: regexp.MustCompile(fmt.Sprintf(botNameRegexFormat, bot.Name())), 164 | handlerMutex: &sync.RWMutex{}, 165 | } 166 | } 167 | 168 | // We may not know our username until after we start running. 169 | func (d *dispatch) RefreshUserName() { 170 | d.botNameRegex = regexp.MustCompile(fmt.Sprintf(botNameRegexFormat, d.robot.Name())) 171 | } 172 | 173 | // appendInOrderWithoutRepeats functions identically to the built-in "append" 174 | // function except that it adds the given string in sorted order and will not 175 | // add duplicates. 176 | // 177 | // This is safe to call with a nil array and is case sensitive. 178 | func appendInOrderWithoutRepeats(array []string, toAdd string) []string { 179 | // find the insert using a binary search 180 | pos := sort.SearchStrings(array, toAdd) 181 | // check if we should add it 182 | if pos == len(array) || array[pos] != toAdd { 183 | // make space 184 | array = append(array, "") 185 | // move over existing data 186 | copy(array[pos+1:], array[pos:]) 187 | // insert new element 188 | array[pos] = toAdd 189 | } 190 | return array 191 | } 192 | 193 | // Commands returns a copy of the internal commands map. 194 | // 195 | // This opens a read lock on the handlerMutex so new commands cannot be added 196 | // while a copy of the commands is made (they will block until processing is 197 | // completed) 198 | func (d *dispatch) Commands() map[string]HandlerDocPair { 199 | d.handlerMutex.RLock() 200 | defer d.handlerMutex.RUnlock() 201 | cmdCopy := make(map[string]HandlerDocPair) 202 | for key, value := range d.commands { 203 | cmdCopy[key] = value 204 | } 205 | return cmdCopy 206 | } 207 | 208 | // EnableHelpCommand registers the default help handler with the bot's command 209 | // map under the name "help". This will log a message if there is already a 210 | // handler registered under that name. 211 | // 212 | // This opens a write lock on the handlerMutex or will wait until one can be 213 | // opened. This is therefore safe to use concurrently with other handler 214 | // functions and/or message processing. 215 | func (d *dispatch) EnableHelpCommand() { 216 | if _, exists := d.commands[helpCommandName]; exists { 217 | log.Println("Enabling built in help command and overriding set help command.") 218 | } 219 | // make a copy of it and use a closure to provide access to the current 220 | // dispatch 221 | helpHandler := defaultHelpHandlerDoc 222 | helpHandler.CmdHandler = func(s State) { 223 | defaultHelpHandler(s, d) 224 | } 225 | d.HandleCommand(&helpHandler) 226 | } 227 | 228 | // HandleCommand adds a given string/handler pair as a new command for the bot. 229 | // This will call the handler function if a string insensitive match succeeds 230 | // on the command name of a message that is considered a potential command 231 | // (either sent @ the bot's name or in a direct message). 232 | // 233 | // This opens a write lock on the handlerMutex or will wait until one can be 234 | // opened. This is therefore safe to use concurrently with other handler 235 | // functions and/or message processing. 236 | func (d *dispatch) HandleCommand(cmd HandlerDocPair) { 237 | d.handlerMutex.Lock() 238 | defer d.handlerMutex.Unlock() 239 | lowerName := strings.ToLower(cmd.Name()) 240 | if _, exists := d.commands[lowerName]; exists { 241 | log.Printf("\"%s\" has been set more than once.", lowerName) 242 | } 243 | newCmd := &HandlerDoc{ 244 | CmdHandler: cmd.Handler(), 245 | CmdName: cmd.Name(), 246 | CmdDescription: cmd.Description(), 247 | CmdUsage: cmd.Usage(), 248 | CmdIsHidden: cmd.IsHidden(), 249 | } 250 | d.commands[lowerName] = newCmd 251 | d.commandNames = appendInOrderWithoutRepeats(d.commandNames, lowerName) 252 | } 253 | 254 | // HandleCommandPattern adds a given pattern to the bot's list of regexp 255 | // commands. This is equivalent to calling HandleCommandRegexp but with a 256 | // non-compiled regular expression. 257 | // 258 | // This uses regexp.MustCompile so it panics if given an invalid regular 259 | // expression. 260 | // 261 | // This opens a write lock on the handlerMutex or will wait until one can be 262 | // opened. This is therefore safe to use concurrently with other handler 263 | // functions and/or message processing. 264 | func (d *dispatch) HandleCommandPattern(pattern string, cmd HandlerDocPair) { 265 | d.HandleCommandRegexp(regexp.MustCompile(pattern), cmd) 266 | } 267 | 268 | // HandleCommandRegexp adds a given pattern to the bot's list of regular 269 | // expression commands. These commands are then checked on any message that is 270 | // considered a potential command (either sent @ the bot's name or in a direct 271 | // message). They are evaluated only after no regular commands match the input 272 | // and then they are checked in the same order as they were added. They are 273 | // only checked against the first word of the message! 274 | // 275 | // This opens a write lock on the handlerMutex or will wait until one can be 276 | // opened. This is therefore safe to use concurrently with other handler 277 | // functions and/or message processing. 278 | func (d *dispatch) HandleCommandRegexp(exp *regexp.Regexp, cmd HandlerDocPair) { 279 | d.handlerMutex.Lock() 280 | defer d.handlerMutex.Unlock() 281 | lowerName := strings.ToLower(cmd.Name()) 282 | if exp == nil { 283 | log.Panicf("Cannot add nil regular expression command under name \"%s\"\n.", lowerName) 284 | return 285 | } 286 | newCmd := &HandlerDoc{ 287 | CmdHandler: cmd.Handler(), 288 | CmdName: cmd.Name(), 289 | CmdDescription: cmd.Description(), 290 | CmdUsage: cmd.Usage(), 291 | CmdIsHidden: cmd.IsHidden(), 292 | cmdRegexp: exp, 293 | } 294 | d.regexpCommands = append(d.regexpCommands, newCmd) 295 | d.commandNames = appendInOrderWithoutRepeats(d.commandNames, lowerName) 296 | } 297 | 298 | // HandleCommandAlias registers a given alias command name to the given 299 | // existing command name. This will silently fail and output a log message 300 | // if the given original command name does not exist or the new alias command 301 | // name is equal to the original command name or the new alias command name is 302 | // already set for the original command. 303 | // 304 | // This is equivalent to calling "HandleCommand" again for the alias command 305 | // with the same documentation as the original command although this also 306 | // registers the alias name with the original HandlerDocPair for help text 307 | // purposes. 308 | // 309 | // This opens a write lock on the handlerMutex or will wait until one can be 310 | // opened. This is therefore safe to use concurrently with other handler 311 | // functions and/or message processing. 312 | func (d *dispatch) HandleCommandAlias(originalName, aliasName string) { 313 | d.handlerMutex.Lock() 314 | lowerOrigName := strings.ToLower(originalName) 315 | doc, exists := d.commands[lowerOrigName] 316 | if !exists { 317 | log.Printf(`Cannot add alias for unset command "%s"`, lowerOrigName) 318 | return 319 | } else if strings.ToLower(originalName) == strings.ToLower(aliasName) { 320 | log.Printf(`A command cannot alias itself (command %s)`, lowerOrigName) 321 | return 322 | } else if !doc.AddAliasName(aliasName) { 323 | log.Printf(`Alias "%s" for original command "%s" already exists`, aliasName, lowerOrigName) 324 | return 325 | } 326 | newDoc := &HandlerDoc{ 327 | CmdName: aliasName, 328 | CmdIsHidden: true, 329 | CmdHandler: doc.Handler(), 330 | CmdDescription: doc.Description(), 331 | CmdUsage: doc.Usage(), 332 | } 333 | // release our lock before actually adding the command 334 | d.handlerMutex.Unlock() 335 | d.HandleCommand(newDoc) 336 | } 337 | 338 | // HandleCommandAliasPattern adds a given pattern to the given commands aliases. 339 | // This is equivalent to calling HandleCommandAliasRegexp but with a 340 | // non-compiled regular expression. 341 | // 342 | // This uses regexp.MustCompile so it panics if given an invalid regular 343 | // expression. 344 | // 345 | // This opens a write lock on the handlerMutex or will wait until one can be 346 | // opened. This is therefore safe to use concurrently with other handler 347 | // functions and/or message processing. 348 | func (d *dispatch) HandleCommandAliasPattern(originalName, aliasName string, pattern string) { 349 | d.HandleCommandAliasRegexp(originalName, aliasName, regexp.MustCompile(pattern)) 350 | } 351 | 352 | // HandleCommandAliasRegexp registers a given alias regexp to the given 353 | // existing command name. This will silently fail and output a log message 354 | // if the given original command name does not exist or the given regexp is nil. 355 | // This will succeed but log a message if the given aliasName is already set. 356 | // 357 | // This is equivalent to calling "HandleCommandRegexp" again for the alias 358 | // command regexp with the same documentation as the original command although 359 | // this also registers the alias name with the original HandlerDocPair for 360 | // help text purposes. 361 | // 362 | // The "aliasName" (second) parameter is used in order to list the alias in the 363 | // help text for the original command. If this should be a "silent" (unlisted) 364 | // alias then call this with the "aliasName" parameter as an empty string ("") 365 | // and it will not be added. 366 | // 367 | // This opens a write lock on the handlerMutex or will wait until one can be 368 | // opened. This is therefore safe to use concurrently with other handler 369 | // functions and/or message processing. 370 | func (d *dispatch) HandleCommandAliasRegexp(originalName, aliasName string, exp *regexp.Regexp) { 371 | d.handlerMutex.Lock() 372 | if exp == nil { 373 | log.Println("Cannot add nil regular expression.") 374 | return 375 | } 376 | lowerOrigName := strings.ToLower(originalName) 377 | doc, exists := d.commands[lowerOrigName] 378 | if !exists { 379 | log.Printf(`Cannot add alias for unset command "%s"`, lowerOrigName) 380 | return 381 | } 382 | if len(aliasName) > 0 && !doc.AddAliasName(aliasName) { 383 | log.Printf( 384 | `Alias "%s" for original command "%s" already exists - regexp was still added`, 385 | aliasName, lowerOrigName) 386 | } 387 | newDoc := &HandlerDoc{ 388 | CmdName: aliasName, 389 | CmdIsHidden: true, 390 | CmdHandler: doc.Handler(), 391 | CmdDescription: doc.Description(), 392 | CmdUsage: doc.Usage(), 393 | } 394 | // release our lock before actually adding the alias 395 | d.handlerMutex.Unlock() 396 | d.HandleCommandRegexp(exp, newDoc) 397 | } 398 | 399 | // HandlePattern adds a given pattern to the bot's list of regexp expressions. 400 | // This is equivalent to calling HandleRegexp but with a non-compiled regular 401 | // expression. 402 | // 403 | // This uses regexp.MustCompile so it panics if given an invalid regular 404 | // expression. 405 | // 406 | // This opens a write lock on the handlerMutex or will wait until one can be 407 | // opened. This is therefore safe to use concurrently with other handler 408 | // functions and/or message processing. 409 | func (d *dispatch) HandlePattern(pattern string, handler HandlerFunc) { 410 | d.HandleRegexp(regexp.MustCompile(pattern), handler) 411 | } 412 | 413 | // HandleRegexp adds a given regular expression to the bot's list of regexp 414 | // expressions. This expression will be checked on every message that is not 415 | // considered a potential command (NOT sent @ the bot and NOT in a 416 | // direct message). If multiple expressions are added then they will be 417 | // evaluated in the order of insertion. 418 | // 419 | // This opens a write lock on the handlerMutex or will wait until one can be 420 | // opened. This is therefore safe to use concurrently with other handler 421 | // functions and/or message processing. 422 | func (d *dispatch) HandleRegexp(exp *regexp.Regexp, handler HandlerFunc) { 423 | d.handlerMutex.Lock() 424 | defer d.handlerMutex.Unlock() 425 | if exp == nil { 426 | log.Println("Cannot add nil regular expression.") 427 | return 428 | } 429 | d.patterns = append(d.patterns, &handlerPair{ 430 | exp: exp, 431 | handle: handler, 432 | }) 433 | } 434 | 435 | // SetDefaultHandler sets a function as the default handler which is called 436 | // when a potential command message (either sent @ the bot's name or in a 437 | // direct message) does not match any of the other set commands. 438 | // 439 | // This opens a write lock on the handlerMutex or will wait until one can be 440 | // opened. This is therefore safe to use concurrently with other handler 441 | // functions and/or message processing. 442 | func (d *dispatch) SetDefaultHandler(handler HandlerFunc) { 443 | d.handlerMutex.Lock() 444 | defer d.handlerMutex.Unlock() 445 | if d.defaultHandler != nil { 446 | log.Println("Default handler has been set more than once.") 447 | } 448 | d.defaultHandler = handler 449 | } 450 | 451 | // ProcessMessage finds a match for a message and runs its Handler. 452 | // If the message is considered a potential command (either sent @ the bot's 453 | // name or in a direct message) then the next word after the bot's name is 454 | // compared to all registered handlers. If one matches then that handler 455 | // function is called with the remaining text seperated by whitespace 456 | // (restpecting quotes). If none match then the default handler is called 457 | // (with an empty fields array). 458 | // 459 | // If the message is not a potential command then it is checked against all 460 | // registered patterns (with an empty fields array upon a match). 461 | // 462 | // This opens a read lock on the handlerMutex so new commands cannot be added 463 | // while a message is being processed (they will block until processing is 464 | // completed) 465 | func (d *dispatch) ProcessMessage(m chat.Message) { 466 | d.handlerMutex.RLock() 467 | defer d.handlerMutex.RUnlock() 468 | defer func() { 469 | if e := recover(); e != nil { 470 | log.Println("Unexpected Panic Processing Message:", m.Text(), " -- Error:", e) 471 | return 472 | } 473 | }() 474 | messageText := m.Text() 475 | nameMatch := d.botNameRegex.FindString(messageText) 476 | if len(nameMatch) > 0 || m.IsDirectMessage() { 477 | // slices are cheap (reference original) so if no match then it's ok 478 | messageText = messageText[len(nameMatch):] 479 | if !d.matchCommands(m, messageText) { 480 | d.callDefault(m, messageText) 481 | } 482 | } else if len(nameMatch) == 0 { 483 | d.matchPatterns(m) 484 | } 485 | } 486 | 487 | // callDefault invokes the default message handler if one is set. 488 | // If one is not set then it logs the unhandled occurrance but otherwise does 489 | // not fail. 490 | // 491 | // This does not acquire a lock on the handlerMutex but one should be acquired 492 | // for reading before calling this method. 493 | func (d *dispatch) callDefault(m chat.Message, messageText string) { 494 | fields := parseFields(messageText) 495 | if d.defaultHandler != nil { 496 | d.defaultHandler.Handle(&state{ 497 | robot: d.robot, 498 | message: m, 499 | fields: fields, 500 | }) 501 | } else { 502 | log.Println("Default handler invoked but none is set.") 503 | } 504 | } 505 | 506 | // matchCommands attempts to match a given message with the map of registered 507 | // commands. It performs case-insensitive matching and will return true upon 508 | // the first match. It expects the second parameter to be the message's text 509 | // with the bot's name and any follwing text up until the next word whitespace 510 | // removed. This returns true if a match is made and false otherwise. 511 | // 512 | // This does not acquire a lock on the handlerMutex but one should be acquired 513 | // for reading before calling this method. 514 | func (d *dispatch) matchCommands(m chat.Message, messageText string) bool { 515 | fullFields := parseFields(messageText) 516 | if len(fullFields) == 0 { 517 | return false 518 | } 519 | commandName := strings.ToLower(fullFields[0]) 520 | fields := fullFields[1:] 521 | command, defined := d.commands[commandName] 522 | if !defined || command.IsRegexpCommand() { 523 | return d.matchCommandRegexp(m, messageText, commandName, fields) 524 | } 525 | command.Handler().Handle(&state{ 526 | robot: d.robot, 527 | message: m, 528 | fields: fields, 529 | }) 530 | return true 531 | } 532 | 533 | // matchCommandRegexp attemps to match a given command word from the given 534 | // message to one of the added regular expression commands. It expects the 535 | // second parameter to be the message's text with the bot's name and any 536 | // following whitespace removed. This returns true if a match is made and false 537 | // otherwise. 538 | // 539 | // This performs a linear search through the slice of regular expression 540 | // commands so their priority is the same as the insertion order. 541 | // 542 | // This does not acquire a lock on the handlerMutex but one should be acquired 543 | // for reading before calling this method. 544 | func (d *dispatch) matchCommandRegexp(m chat.Message, messageText, commandName string, fields []string) bool { 545 | cmd := d.findCommandRegexp(commandName) 546 | if cmd == nil { 547 | return false 548 | } 549 | cmd.Handler().Handle(&state{ 550 | robot: d.robot, 551 | message: m, 552 | fields: fields, 553 | }) 554 | return true 555 | 556 | } 557 | 558 | // findCommandRegexp searches the dispatch's internal slice of regexpCommands 559 | // by attempting to match the given string to all registered regexp commands. 560 | // It does this by performing a linear search through the slice and therefore 561 | // searches in order of insertion. This is safe to call if the internal slice 562 | // of command regexps is nil. 563 | // 564 | // This does not acquire a lock on the handlerMutex but one should be acquired 565 | // for reading before calling this method. 566 | func (d *dispatch) findCommandRegexp(commandPart string) HandlerDocPair { 567 | for _, cmd := range d.regexpCommands { 568 | if cmd.Regexp().MatchString(commandPart) { 569 | return cmd 570 | } 571 | } 572 | return nil 573 | } 574 | 575 | // matchPatterns iterates through the array of registered regular expressions 576 | // (patterns) in the order of insertion and checks if they match any part of 577 | // the given message's text. If they do then they are invoked with an empty 578 | // fields array. 579 | // 580 | // This does not acquire a lock on the handlerMutex but one should be acquired 581 | // for reading before calling this method. 582 | func (d *dispatch) matchPatterns(m chat.Message) bool { 583 | for _, pair := range d.patterns { 584 | if pair.Exp().MatchString(m.Text()) { 585 | pair.Handler().Handle(&state{ 586 | robot: d.robot, 587 | message: m, 588 | }) 589 | return true 590 | } 591 | } 592 | return false 593 | } 594 | 595 | // defaultHelpHandler either shows all available (and non-hidden) commands or 596 | // the help text for a given command. 597 | // 598 | // This opens a read lock on the handlerMutex so new commands cannot be added 599 | // while a message is being processed (they will block until processing is 600 | // completed) 601 | func defaultHelpHandler(s State, d *dispatch) { 602 | d.handlerMutex.RLock() 603 | defer d.handlerMutex.RUnlock() 604 | if len(s.Fields()) == 0 { 605 | showAllCommands(s, d) 606 | } else { 607 | showCommandHelp(s, d) 608 | } 609 | } 610 | 611 | // showAllCommands is used by the default help handler to show a list of all 612 | // non-hidden commands regsitered to the dispatch. 613 | // 614 | // This does not acquire a lock on the handlerMutex but one should be acquired 615 | // for reading before calling this method. 616 | func showAllCommands(s State, d *dispatch) { 617 | if len(d.commandNames) == 0 { 618 | s.Chat().Send(s.Message().Channel().ID(), "No commands have been set!") 619 | return 620 | } 621 | var buf bytes.Buffer 622 | buf.WriteString("Available commands:\n") 623 | buf.WriteString(">>>") 624 | for _, name := range d.commandNames { 625 | docPair, ok := d.commands[name] 626 | if !ok || docPair.IsHidden() { 627 | continue 628 | } 629 | buf.WriteString(fmt.Sprintf("*%s*", docPair.Name())) 630 | if len(docPair.Description()) > 0 { 631 | buf.WriteString(fmt.Sprintf(" - _%s_", docPair.Description())) 632 | } 633 | buf.WriteString("\n") 634 | } 635 | buf.WriteString("\nFor help with a command, type `help [command name]`.") 636 | s.Reply(buf.String()) 637 | } 638 | 639 | // showCommandHelp shows the description, usage, and aliases if they are set 640 | // for a given command name. The command name should be the first element in 641 | // the state's Fields. 642 | // 643 | // This does not acquire a lock on the handlerMutex but one should be acquired 644 | // for reading before calling this method. 645 | func showCommandHelp(s State, d *dispatch) { 646 | if len(s.Fields()) == 0 { 647 | return 648 | } 649 | cmdName := strings.ToLower(s.Fields()[0]) 650 | docPair, exists := d.commands[cmdName] 651 | if !exists { 652 | docPair = d.findCommandRegexp(cmdName) 653 | if docPair == nil { 654 | textFmt := "Unrecognized command _%s_. Type *`help`* to view a list of all available commands." 655 | s.Chat().Send(s.Message().Channel().ID(), fmt.Sprintf(textFmt, cmdName)) 656 | return 657 | } 658 | } 659 | var buf bytes.Buffer 660 | buf.WriteString(fmt.Sprintf("*%s*", cmdName)) 661 | if len(docPair.Description()) > 0 { 662 | buf.WriteString(fmt.Sprintf(" - _%s_", docPair.Description())) 663 | } 664 | buf.WriteString("\n\n") 665 | aliasNames := docPair.AliasNames() 666 | if len(aliasNames) > 0 { 667 | buf.WriteString("Alias: _") 668 | for i := range aliasNames { 669 | buf.WriteString(aliasNames[i]) 670 | if i+1 < len(aliasNames) { 671 | buf.WriteString(", ") 672 | } 673 | } 674 | buf.WriteString("_\n") 675 | } 676 | if len(docPair.Usage()) > 0 { 677 | buf.WriteString(">>>") 678 | for _, use := range docPair.Usage() { 679 | buf.WriteString(cmdName) 680 | buf.WriteString(" ") 681 | buf.WriteString(use) 682 | buf.WriteString("\n") 683 | } 684 | } 685 | s.Reply(buf.String()) 686 | } 687 | 688 | var quoteCharacters = &unicode.RangeTable{ 689 | R16: []unicode.Range16{ 690 | unicode.Range16{Lo: '"', Hi: '"', Stride: 1}, 691 | }, 692 | } 693 | 694 | func parseFields(input string) []string { 695 | fields := make([]string, 0, 10) 696 | skipSpaces := true 697 | fieldStart := -1 698 | 699 | for i, r := range input { 700 | if unicode.In(r, quoteCharacters) { 701 | if fieldStart == -1 { 702 | // start field 703 | fieldStart = i + 1 704 | skipSpaces = false 705 | } else { 706 | // end field 707 | fields = append(fields, input[fieldStart:i]) 708 | fieldStart = -1 709 | skipSpaces = true 710 | } 711 | } else if unicode.IsSpace(r) && skipSpaces { 712 | // end field if not in a quoted field 713 | if fieldStart != -1 { 714 | fields = append(fields, input[fieldStart:i]) 715 | fieldStart = -1 716 | } 717 | } else if fieldStart == -1 { 718 | // start field 719 | fieldStart = i 720 | } 721 | } 722 | if fieldStart != -1 { 723 | // end last field if it hasn't yet 724 | fields = append(fields, input[fieldStart:]) 725 | } 726 | return fields 727 | } 728 | --------------------------------------------------------------------------------