From d016448b0bc46af7713602ed56715526ee6e2ba3 Mon Sep 17 00:00:00 2001 From: Seth Etter Date: Tue, 18 Oct 2022 06:26:13 -0500 Subject: [PATCH 1/2] events this week functionality --- engine/engine.go | 70 ++++++++++++++++++++++++++++++------------- engine/engine_test.go | 34 ++++++++++++++++++--- main.go | 28 +++++++++++++---- rules/rules.go | 8 +++-- sources/meetup.go | 16 +++++++--- sources/sources.go | 2 ++ 6 files changed, 121 insertions(+), 37 deletions(-) diff --git a/engine/engine.go b/engine/engine.go index cefd6bc..9391f69 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -11,12 +11,15 @@ import ( ) type EngineConfig struct { - Channels []channels.Channel - Sources []sources.Source - Rules []rules.NotifyRule - RunAt RunAt - Location *time.Location - DebugMode bool + NowFunc func() time.Time + Channels []channels.Channel + Sources []sources.Source + Rules []rules.NotifyRule + WeeklySummaryTemplates map[string]rules.WeeklySummaryFunc + WeeklySummaryDay time.Weekday + RunAt RunAt + Location *time.Location + DebugMode bool } type RunAt struct { @@ -63,9 +66,16 @@ func (e *Engine) RunOnce() { continue } + eventsInNextWeek := []sources.Event{} + dayOfWeek := e.config.NowFunc().Weekday() + for _, event := range events { + if dayOfWeek == event.DateTime.Weekday() && eventIsWithinNextWeek(event, e.config.NowFunc()) { + eventsInNextWeek = append(eventsInNextWeek, event) + } + for _, rule := range e.config.Rules { - if !rule.EventIsApplicable(event, e.config.Location) { + if !rule.EventIsApplicable(event, e.config.Location, e.config.NowFunc()) { continue } @@ -79,22 +89,42 @@ func (e *Engine) RunOnce() { if !ok { log.Println(fmt.Errorf("did not find message for %s channel %s", channel.Type(), channel.Name())) } + e.sendMessage(channel, msg) + } + } + } - log.Printf("sending event to %s on %s: %s\n", channel.Name(), channel.Type(), event.Name) - if e.config.DebugMode { - fmt.Printf("%s\n\n", msg) - } else { - if err := channel.Send(msg); err != nil { - log.Println(fmt.Errorf( - "failed to send to %s channel %s: %w", - channel.Type(), - channel.Name(), - err, - )) - } - } + if dayOfWeek == e.config.WeeklySummaryDay { + for _, channel := range e.config.Channels { + msgFunc, ok := e.config.WeeklySummaryTemplates[channel.Type()] + if !ok { + log.Println(fmt.Errorf("did not find message for %s channel %s", channel.Type(), channel.Name())) + } else { + e.sendMessage(channel, msgFunc(eventsInNextWeek)) } } } } } + +func (e *Engine) sendMessage(channel channels.Channel, message string) { + log.Printf("sending weekly summary to %s on %s\n", channel.Name(), channel.Type()) + + if e.config.DebugMode { + fmt.Printf("%s\n\n", message) + } else { + if err := channel.Send(message); err != nil { + log.Println(fmt.Errorf( + "failed to send to %s channel %s: %w", + channel.Type(), + channel.Name(), + err, + )) + } + } +} + +func eventIsWithinNextWeek(event sources.Event, now time.Time) bool { + diff := now.Sub(event.DateTime) + return diff < 7*24*time.Hour +} diff --git a/engine/engine_test.go b/engine/engine_test.go index 883dbb2..3516a67 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -2,6 +2,7 @@ package engine_test import ( "fmt" + "strings" "testing" "time" @@ -11,6 +12,8 @@ import ( "github.com/devict/promobot/sources" ) +var octoberFirst = time.Date(2012, time.October, 1, 8, 0, 0, 0, time.Local) + type TestChannel struct { name string sentMsgs []string @@ -36,8 +39,10 @@ func NewTestSource(name string, events []sources.Event) *TestSource { return &TestSource{name: name, events: events} } -func (c *TestSource) Name() string { return c.name } -func (c *TestSource) Type() string { return "test" } +func (c *TestSource) Name() string { return c.name } +func (c *TestSource) Type() string { return "test" } +func (c *TestSource) JsonUrl() string { return "nope" } +func (c *TestSource) HtmlUrl() string { return "https://meetup.com/devict" } func (c *TestSource) Retrieve(t *time.Location) ([]sources.Event, error) { return c.events, nil } @@ -48,7 +53,7 @@ func testEvent(name, source string, daysAhead int) sources.Event { Source: source, URL: "https://devict.org", Location: "Definitely not the metaverse", - DateTime: time.Now().Add(time.Duration(daysAhead*24) * time.Hour), + DateTime: octoberFirst.Add(time.Duration(daysAhead*24) * time.Hour), } } @@ -57,6 +62,7 @@ func TestEngine(t *testing.T) { testChannel2 := NewTestChannel("test2") config := engine.EngineConfig{ + NowFunc: func() time.Time { return octoberFirst }, Channels: []channels.Channel{ channels.Channel(testChannel1), channels.Channel(testChannel2), @@ -73,6 +79,17 @@ func TestEngine(t *testing.T) { testEvent("Test Event 8", "test-source-1", 8), })), }, + WeeklySummaryDay: octoberFirst.Weekday(), + WeeklySummaryTemplates: map[string]rules.WeeklySummaryFunc{ + "test": func(events []sources.Event) string { + eventLines := []string{} + for _, event := range events { + day := event.DateTime.Weekday().String() + eventLines = append(eventLines, fmt.Sprintf("- [%s] <%s|%s>", day[:3], event.URL, event.Name)) + } + return fmt.Sprintf("Events this week!\n\n%s", strings.Join(eventLines, "\n")) + }, + }, Rules: []rules.NotifyRule{ { NumDaysOut: 1, @@ -108,6 +125,15 @@ func TestEngine(t *testing.T) { "1 day until Test Event 1 from test-source-1", "3 days until Test Event 3 from test-source-1", "7 days until Test Event 7 from test-source-1", + strings.Join([]string{ + "Events this week!\n", + "- [Tue] ", + "- [Wed] ", + "- [Thu] ", + "- [Fri] ", + "- [Sat] ", + "- [Sun] ", + }, "\n"), } if len(testChannel1.sentMsgs) != len(expected) { @@ -138,7 +164,7 @@ func containsStr(slice []string, str string) bool { } func TestShouldRun(t *testing.T) { - now := time.Now() + now := octoberFirst e := engine.NewEngine(engine.EngineConfig{ RunAt: engine.RunAt{ diff --git a/main.go b/main.go index 7d1b21d..baa9d7f 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "time" "github.com/devict/promobot/channels" @@ -19,9 +20,9 @@ type config struct { DevICTTwitter DevICTTwitterConfig // Sources - DevICTMeetupURL string `envconfig:"DEVICT_MEETUP_URL" required:"true"` - OzSecMeetupURL string `envconfig:"OZSEC_MEETUP_URL" required:"true"` - WTFMeetupURL string `envconfig:"WTF_MEETUP_URL" required:"true"` + DevICTMeetupURLName string `envconfig:"DEVICT_MEETUP_URL_NAME" required:"true"` + OzSecMeetupURLName string `envconfig:"OZSEC_MEETUP_URL_NAME" required:"true"` + WTFMeetupURLName string `envconfig:"WTF_MEETUP_URL_NAME" required:"true"` } type DevICTTwitterConfig struct { @@ -40,10 +41,11 @@ func main() { loc, _ := time.LoadLocation("America/Chicago") e := engine.NewEngine(engine.EngineConfig{ + NowFunc: func() time.Time { return time.Now() }, Sources: []sources.Source{ - sources.Source(sources.NewMeetupSource("devICT", c.DevICTMeetupURL)), - sources.Source(sources.NewMeetupSource("OzSec", c.OzSecMeetupURL)), - sources.Source(sources.NewMeetupSource("Wichita Technology Forum", c.WTFMeetupURL)), + sources.Source(sources.NewMeetupSource("devICT", c.DevICTMeetupURLName)), + sources.Source(sources.NewMeetupSource("OzSec", c.OzSecMeetupURLName)), + sources.Source(sources.NewMeetupSource("Wichita Technology Forum", c.WTFMeetupURLName)), }, Channels: []channels.Channel{ channels.Channel(channels.NewSlackChannel("devICT", c.DevICTSlackWebhook)), @@ -54,6 +56,20 @@ func main() { APISecretKey: c.DevICTTwitter.APISecretKey, })), }, + WeeklySummaryTemplates: map[string]rules.WeeklySummaryFunc{ + "slack": func(events []sources.Event) string { + eventLines := []string{} + for _, event := range events { + day := event.DateTime.Weekday().String() + eventLines = append(eventLines, fmt.Sprintf("- [%s] <%s|%s>", day[:3], event.URL, event.Name)) + } + return fmt.Sprintf("Events this week!\n\n%s", strings.Join(eventLines, "\n")) + }, + // "twitter": func(events []sources.Event) string { + // return "" + // }, + }, + WeeklySummaryDay: time.Tuesday, Rules: []rules.NotifyRule{ { NumDaysOut: 10, diff --git a/rules/rules.go b/rules/rules.go index 5739d14..f223dcd 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -8,6 +8,8 @@ import ( type MsgFunc func(sources.Event) string +type WeeklySummaryFunc func([]sources.Event) string + type NotifyRule struct { NumDaysOut int ChannelTemplates map[string]MsgFunc @@ -21,13 +23,13 @@ func (rule NotifyRule) MessagesFromEvent(event sources.Event) (map[string]string return channelMessages, nil } -func (rule NotifyRule) EventIsApplicable(event sources.Event, loc *time.Location) bool { +func (rule NotifyRule) EventIsApplicable(event sources.Event, loc *time.Location, now time.Time) bool { // don't promote the event if it's already started, in case of 0 days out rule - if time.Now().In(loc).After(event.DateTime) { + if now.In(loc).After(event.DateTime) { return false } - checkDate := dateFromTime(time.Now().Add(time.Duration(rule.NumDaysOut*24) * time.Hour)) + checkDate := dateFromTime(now.Add(time.Duration(rule.NumDaysOut*24) * time.Hour)) eventDate := dateFromTime(event.DateTime) return eventDate.Equal(checkDate) } diff --git a/sources/meetup.go b/sources/meetup.go index b6a0981..fce68af 100644 --- a/sources/meetup.go +++ b/sources/meetup.go @@ -2,6 +2,7 @@ package sources import ( "encoding/json" + "fmt" "io" "net/http" "time" @@ -9,21 +10,28 @@ import ( type MeetupSource struct { name string - url string + urlName string + } -func NewMeetupSource(name, url string) *MeetupSource { +func NewMeetupSource(name, urlName string) *MeetupSource { return &MeetupSource{ name: name, - url: url, + urlName: urlName, } } func (c *MeetupSource) Name() string { return c.name } func (c *MeetupSource) Type() string { return "meetup" } +func (c *MeetupSource) JsonUrl() string { + return fmt.Sprintf("https://api.meetup.com/2/events?&sign=true&photo-host=public&group_urlname=%s&limited_events=false&fields=series&status=upcoming&page=20", c.urlName) +} +func (c *MeetupSource) HtmlUrl() string { + return fmt.Sprintf("https://meetup.com/%s", c.urlName) +} func (c *MeetupSource) Retrieve(loc *time.Location) ([]Event, error) { - resp, err := http.Get(c.url) + resp, err := http.Get(c.JsonUrl()) if err != nil { return []Event{}, err } diff --git a/sources/sources.go b/sources/sources.go index 6b4f131..b56d0e4 100644 --- a/sources/sources.go +++ b/sources/sources.go @@ -5,6 +5,8 @@ import "time" type Source interface { Name() string Type() string + JsonUrl() string + HtmlUrl() string Retrieve(*time.Location) ([]Event, error) } From 64141ef959959fec7f8ee3ba70fbd52b5532c70a Mon Sep 17 00:00:00 2001 From: Seth Etter Date: Tue, 18 Oct 2022 06:35:02 -0500 Subject: [PATCH 2/2] fix gating logic --- engine/engine.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/engine/engine.go b/engine/engine.go index 9391f69..7110aa3 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -70,7 +70,7 @@ func (e *Engine) RunOnce() { dayOfWeek := e.config.NowFunc().Weekday() for _, event := range events { - if dayOfWeek == event.DateTime.Weekday() && eventIsWithinNextWeek(event, e.config.NowFunc()) { + if eventIsWithinNextWeek(event, e.config.NowFunc()) { eventsInNextWeek = append(eventsInNextWeek, event) } @@ -126,5 +126,6 @@ func (e *Engine) sendMessage(channel channels.Channel, message string) { func eventIsWithinNextWeek(event sources.Event, now time.Time) bool { diff := now.Sub(event.DateTime) - return diff < 7*24*time.Hour + inNextWeek := diff < 7*24*time.Hour + return inNextWeek }