diff --git a/companion/adapter/inbound/yamlconfig/loader.go b/companion/adapter/inbound/yamlconfig/loader.go index 316c4d0..165e699 100644 --- a/companion/adapter/inbound/yamlconfig/loader.go +++ b/companion/adapter/inbound/yamlconfig/loader.go @@ -38,8 +38,14 @@ func (l *loader) Load() (*companion.Config, error) { return nil, errors.New("need either a twitch channel or a streamlabs token. see config.example.yaml for an example") } - if c.Duration < 500*time.Millisecond { - return nil, errors.New("spray time must be at least 500ms") + if len(c.Duration) == 0 { + c.Duration = []time.Duration{1 * time.Second} + } + + for _, duration := range c.Duration { + if duration < 500*time.Millisecond { + duration = 500 * time.Millisecond + } } if len(c.Events) == 0 && len(c.ChatTriggers) == 0 { @@ -63,7 +69,7 @@ func (l *loader) Example() { c := &companion.Config{ Cooldown: 5 * time.Second, - Duration: 1 * time.Second, + Duration: []time.Duration{1 * time.Second}, Squirters: []string{ "192.168.1.200", }, @@ -75,6 +81,16 @@ func (l *loader) Example() { {Type: companion.EventTypeDono, Min: 20, Max: 30}, {Type: companion.EventTypeDono, Min: 100}, {Type: companion.EventTypeSub, Min: 10}, + {Type: companion.EventTypeSub, Min: 25, Pattern: companion.SquirtPattern{ + 3 * time.Second, + }}, + {Type: companion.EventTypeSub, Min: 50, Pattern: companion.SquirtPattern{ + 1 * time.Second, + 500 * time.Millisecond, + 2 * time.Second, + 500 * time.Millisecond, + 3 * time.Second, + }}, }, ChatTriggers: []companion.ChatTrigger{ { diff --git a/companion/cmd/main.go b/companion/cmd/main.go index a224c92..62d4fdf 100644 --- a/companion/cmd/main.go +++ b/companion/cmd/main.go @@ -43,6 +43,7 @@ func main() { os.Exit(1) } } + //conf.Dump(os.Stdout) converter, err := exchangerate.New(conf.Currency) if err != nil { @@ -93,21 +94,34 @@ func main() { timeout := time.Now() + patternSquirt := func(p companion.SquirtPattern) { + timeout = time.Now() + for _, d := range p { + timeout = timeout.Add(d) + } + + for i, d := range p { + if i%2 == 0 { + ui.SetActive(d) + squirters.Squirt(d) + } + time.Sleep(d) + } + } + for { select { case m := <-messages: - if conf.HasChatTrigger(m) && time.Now().After(timeout) { + hasTrigger, pattern := conf.GetChatTrigger(m) + if hasTrigger && time.Now().After(timeout) { log.Printf("Message from %v: %v -> Squirt for %v", m.User, m.Message, conf.Duration) - ui.SetActive(conf.Duration) - squirters.Squirt(conf.Duration) - timeout = time.Now().Add(conf.Cooldown + conf.Duration) + go patternSquirt(*pattern) } case e := <-events: - if conf.HasEvent(e) && time.Now().After(timeout) { + hasEvent, pattern := conf.GetEvent(e) + if hasEvent && time.Now().After(timeout) { log.Printf("Got %v (%v): Squirt for %v", e.EventType, e.Amount, conf.Duration) - ui.SetActive(conf.Duration) - squirters.Squirt(conf.Duration) - timeout = time.Now().Add(conf.Cooldown + conf.Duration) + go patternSquirt(*pattern) } case <-ui.OnQuit(): log.Println("Quitting!") @@ -119,13 +133,16 @@ func main() { } func setupLogging() error { + if os.Getenv("LOG_CONSOLE") == "" { - file, err := os.OpenFile("companion.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModePerm) - if err != nil { - return err - } + file, err := os.OpenFile("companion.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModePerm) + if err != nil { + return err + } + + log.SetOutput(file) - log.SetOutput(file) + } return nil } diff --git a/companion/config.go b/companion/config.go index b954990..e771596 100644 --- a/companion/config.go +++ b/companion/config.go @@ -3,6 +3,7 @@ package companion import ( "gopkg.in/yaml.v3" "io" + "log" "strings" "time" ) @@ -21,7 +22,7 @@ type ConfigLoader interface { type Config struct { Cooldown time.Duration `yaml:"cooldown"` - Duration time.Duration `yaml:"duration"` + Duration SquirtPattern `yaml:"duration"` Twitch string `yaml:"twitch"` Streamlabs string `yaml:"streamlabs"` @@ -34,19 +35,25 @@ type Config struct { } type Event struct { - Type EventType `yaml:"type"` - Min int `yaml:"min"` - Max int `yaml:"max,omitempty"` + Type EventType `yaml:"type"` + Min int `yaml:"min"` + Max int `yaml:"max,omitempty"` + Pattern SquirtPattern `yaml:"duration,omitempty"` } type ChatTrigger struct { - Role ChatRole `yaml:"role"` - User string `yaml:"user,omitempty"` - Message string `yaml:"message"` + Role ChatRole `yaml:"role"` + User string `yaml:"user,omitempty"` + Message string `yaml:"message"` + Pattern SquirtPattern `yaml:"duration,omitempty"` } -func (c Config) HasEvent(ev StreamEvent) bool { - for _, e := range c.Events { +func (c Config) GetEvent(ev StreamEvent) (bool, *SquirtPattern) { + defaultPattern := c.Duration + + var match *Event + + for i, e := range c.Events { if ev.EventType != e.Type { continue @@ -60,13 +67,30 @@ func (c Config) HasEvent(ev StreamEvent) bool { continue } - return true + log.Println("found a match: ", e) + + if match == nil { + match = &c.Events[i] + } + + if e.Min > match.Min { + match = &e + } } - return false + if match != nil { + if len(match.Pattern) == 0 { + return true, &defaultPattern + } else { + return true, &match.Pattern + } + } + + return false, nil } -func (c Config) HasChatTrigger(message ChatMessage) bool { +func (c Config) GetChatTrigger(message ChatMessage) (bool, *SquirtPattern) { + defaultPattern := c.Duration for _, e := range c.ChatTriggers { if message.Role < e.Role { @@ -81,10 +105,14 @@ func (c Config) HasChatTrigger(message ChatMessage) bool { continue } - return true + if len(e.Pattern) == 0 { + return true, &defaultPattern + } else { + return true, &e.Pattern + } } - return false + return false, nil } func (c Config) Dump(o io.Writer) { diff --git a/companion/config_test.go b/companion/config_test.go index 8a9a5c8..55ad23e 100644 --- a/companion/config_test.go +++ b/companion/config_test.go @@ -2,102 +2,152 @@ package companion import ( "fmt" + "reflect" "testing" + "time" ) func TestConfig_HasEvent(t *testing.T) { - c := Config{Events: []Event{ - { - Type: EventTypeBits, - Min: 0, - Max: 100, - }, - { - Type: EventTypeBits, - Min: 200, - Max: 250, - }, - { - Type: EventTypeDono, - Min: 20, - Max: 30, - }, - { - Type: EventTypeDono, - Min: 100, - }, - }} + sqDefault := SquirtPattern{500 * time.Millisecond} + sqOne := SquirtPattern{1 * time.Second, 2 * time.Second, 1 * time.Second} + sqTwo := SquirtPattern{2 * time.Second} + sqThree := SquirtPattern{3 * time.Second} + sqFour := SquirtPattern{4 * time.Second} + + c := Config{ + Duration: sqDefault, + Events: []Event{ + { + Type: EventTypeBits, + Min: 0, + Max: 100, + Pattern: sqOne, + }, + { + Type: EventTypeBits, + Min: 200, + Max: 250, + Pattern: sqTwo, + }, + { + Type: EventTypeDono, + Min: 20, + Max: 30, + Pattern: sqThree, + }, + { + Type: EventTypeDono, + Min: 100, + Pattern: sqFour, + }, + { + Type: EventTypeDono, + Min: 200, + }, + }} tests := []struct { - ev StreamEvent - want bool + ev StreamEvent + want bool + wantPattern *SquirtPattern }{ - {StreamEvent{EventTypeBits, 50}, true}, - {StreamEvent{EventTypeBits, 150}, false}, - {StreamEvent{EventTypeBits, 220}, true}, - {StreamEvent{EventTypeDono, 15}, false}, - {StreamEvent{EventTypeDono, 25}, true}, - {StreamEvent{EventTypeDono, 150}, true}, - {StreamEvent{EventTypeDono, 500}, true}, + {StreamEvent{EventTypeBits, 50}, true, &sqOne}, + {StreamEvent{EventTypeBits, 150}, false, nil}, + {StreamEvent{EventTypeBits, 220}, true, &sqTwo}, + {StreamEvent{EventTypeDono, 15}, false, nil}, + {StreamEvent{EventTypeDono, 25}, true, &sqThree}, + {StreamEvent{EventTypeDono, 150}, true, &sqFour}, + {StreamEvent{EventTypeDono, 500}, true, &sqDefault}, } + for i, tt := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { - if got := c.HasEvent(tt.ev); got != tt.want { + got, p := c.GetEvent(tt.ev) + if got != tt.want { t.Errorf("HasEvent() = %v, want %v", got, tt.want) } + + t.Logf("Want: %v", tt.wantPattern) + t.Logf("Got: %v", p) + + if !reflect.DeepEqual(p, tt.wantPattern) { + t.Errorf("HasEvent() = %v, want pattern %v", p, tt.wantPattern) + } }) } } func TestConfig_HasChatTrigger(t *testing.T) { - c := Config{ChatTriggers: []ChatTrigger{ - { - Role: ChatRoleMod, - Message: "modsquirt", - }, - { - Role: ChatRoleSub, - Message: "subsquirt", - }, - { - Role: ChatRolePleb, - Message: "plebsquirt", - }, - { - User: "Steven", - Message: "stevensquirt", - }, - { - User: "Tom", - Role: ChatRoleSub, - Message: "tomsquirt", - }, - }} + + sqDefault := SquirtPattern{500 * time.Millisecond} + sqOne := SquirtPattern{1 * time.Second} + sqTwo := SquirtPattern{2 * time.Second} + sqThree := SquirtPattern{3 * time.Second} + sqFour := SquirtPattern{4 * time.Second} + + c := Config{ + Duration: sqDefault, + ChatTriggers: []ChatTrigger{ + { + Role: ChatRoleMod, + Message: "modsquirt", + Pattern: sqOne, + }, + { + Role: ChatRoleSub, + Message: "subsquirt", + Pattern: sqTwo, + }, + { + Role: ChatRolePleb, + Message: "plebsquirt", + Pattern: sqThree, + }, + { + User: "Steven", + Message: "stevensquirt", + Pattern: sqFour, + }, + { + User: "Tom", + Role: ChatRoleSub, + Message: "tomsquirt", + }, + }} tests := []struct { - message ChatMessage - want bool + message ChatMessage + want bool + wantPattern *SquirtPattern }{ - {ChatMessage{"Steven", ChatRoleMod, "do a modsquirt"}, true}, - {ChatMessage{"Steven", ChatRoleSub, "do a vipsquirt"}, false}, - {ChatMessage{"Steven", ChatRoleSub, "do a subsquirt"}, true}, - {ChatMessage{"Steven", ChatRolePleb, "do a subsquirt"}, false}, - {ChatMessage{"Steven", ChatRolePleb, "do a plebsquirt"}, true}, - {ChatMessage{"Steven", ChatRolePleb, "do a stevensquirt"}, true}, - {ChatMessage{"Peter", ChatRolePleb, "do a stevensquirt"}, false}, - {ChatMessage{"Tom", ChatRoleMod, "do a tomsquirt"}, true}, - {ChatMessage{"Tom", ChatRoleSub, "do a tomsquirt"}, true}, - {ChatMessage{"Tom", ChatRolePleb, "do a tomsquirt"}, false}, - {ChatMessage{"Karl", ChatRoleSub, "do a tomsquirt"}, false}, - {ChatMessage{"Karl", ChatRoleMod, "do a tomsquirt"}, false}, + {ChatMessage{"Steven", ChatRoleMod, "do a modsquirt"}, true, &sqOne}, + {ChatMessage{"Steven", ChatRoleSub, "do a vipsquirt"}, false, nil}, + {ChatMessage{"Steven", ChatRoleSub, "do a subsquirt"}, true, &sqTwo}, + {ChatMessage{"Steven", ChatRolePleb, "do a subsquirt"}, false, nil}, + {ChatMessage{"Steven", ChatRolePleb, "do a plebsquirt"}, true, &sqThree}, + {ChatMessage{"Steven", ChatRolePleb, "do a stevensquirt"}, true, &sqFour}, + {ChatMessage{"Peter", ChatRolePleb, "do a stevensquirt"}, false, nil}, + {ChatMessage{"Tom", ChatRoleMod, "do a tomsquirt"}, true, &sqDefault}, + {ChatMessage{"Tom", ChatRoleSub, "do a tomsquirt"}, true, &sqDefault}, + {ChatMessage{"Tom", ChatRolePleb, "do a tomsquirt"}, false, nil}, + {ChatMessage{"Karl", ChatRoleSub, "do a tomsquirt"}, false, nil}, + {ChatMessage{"Karl", ChatRoleMod, "do a tomsquirt"}, false, nil}, } for i, tt := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { - if got := c.HasChatTrigger(tt.message); got != tt.want { + got, p := c.GetChatTrigger(tt.message) + if got != tt.want { t.Errorf("HasChatTrigger() = %v, want %v\n%v", got, tt.want, tt.message) } + + t.Logf("Want: %v", tt.wantPattern) + t.Logf("Got: %v", p) + + if !reflect.DeepEqual(p, tt.wantPattern) { + t.Errorf("HasChatTrigger() = %v, want pattern %v\n%v", p, tt.wantPattern, tt.message) + } }) } } diff --git a/companion/squirtpattern.go b/companion/squirtpattern.go new file mode 100644 index 0000000..eb4d55b --- /dev/null +++ b/companion/squirtpattern.go @@ -0,0 +1,38 @@ +package companion + +import ( + "gopkg.in/yaml.v3" + "time" +) + +type SquirtPattern []time.Duration + +func (s SquirtPattern) MarshalYAML() (interface{}, error) { + p := s + switch len(p) { + case 0: + return nil, nil + case 1: + return p[0], nil + default: + return []time.Duration(p), nil + } +} + +func (s *SquirtPattern) UnmarshalYAML(value *yaml.Node) error { + var p []time.Duration + switch value.Kind { + case yaml.ScalarNode: + if duration, err := time.ParseDuration(value.Value); err == nil { + p = append(p, duration) + } + case yaml.SequenceNode: + for _, node := range value.Content { + if duration, err := time.ParseDuration(node.Value); err == nil { + p = append(p, duration) + } + } + } + *s = p + return nil +}