From 5388480f3146e22fb9997d4276a4c9e4326ed055 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Tue, 25 Apr 2023 11:51:12 +0200 Subject: [PATCH] Overhaul logging and error handling --- discord.go | 36 +++++++++++++++++++++------- hn.go | 37 ++++++++++++++++++----------- main.go | 28 +++++++++++++++++++--- sn.go | 70 +++++++++++++++++++++++++++++++++++------------------- sn_test.go | 6 ++++- 5 files changed, 126 insertions(+), 51 deletions(-) diff --git a/discord.go b/discord.go index b4ed143..49a9a32 100644 --- a/discord.go +++ b/discord.go @@ -54,7 +54,6 @@ func initBot() { } func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { - // Ignore all messages created by the bot itself if m.Author.ID == s.State.User.ID { return } @@ -62,15 +61,15 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { if err != nil { return } - story := FetchStoryById(hackerNewsId) + story, err := FetchStoryById(hackerNewsId) _, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false}) if err != nil { var dupesErr *DupesError if errors.As(err, &dupesErr) { SendDupesErrorToDiscord(hackerNewsId, dupesErr) - } else { - log.Fatal("unexpected error returned") + return } + SendErrorToDiscord(err) } } @@ -83,7 +82,7 @@ func onMessageReact(s *discordgo.Session, reaction *discordgo.MessageReactionAdd } m, err := s.ChannelMessage(reaction.ChannelID, reaction.MessageID) if err != nil { - log.Println("error:", err) + SendErrorToDiscord(err) return } if len(m.Embeds) == 0 { @@ -97,14 +96,21 @@ func onMessageReact(s *discordgo.Session, reaction *discordgo.MessageReactionAdd if err != nil { return } - story := FetchStoryById(id) + story, err := FetchStoryById(id) + if err != nil { + SendErrorToDiscord(err) + return + } id, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: true}) if err != nil { - log.Fatal("unexpected error returned") + SendErrorToDiscord(err) } } func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) { + msg := fmt.Sprint(dupesErr) + log.Println(msg) + title := fmt.Sprintf("%d dupe(s) found for %s:", len(dupesErr.Dupes), dupesErr.Url) color := 0xffc107 var fields []*discordgo.MessageEmbedField @@ -147,6 +153,7 @@ func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) { }, ) } + embed := discordgo.MessageEmbed{ Title: title, Color: color, @@ -162,6 +169,19 @@ func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) { func SendEmbedToDiscord(embed *discordgo.MessageEmbed) { _, err := dg.ChannelMessageSendEmbed(DiscordChannelId, embed) if err != nil { - log.Fatal("Error during json.Marshal:", err) + err = fmt.Errorf("error during sending embed: %w", err) + log.Println(err) } } + +func SendErrorToDiscord(err error) { + msg := fmt.Sprint(err) + log.Println(msg) + + embed := discordgo.MessageEmbed{ + Title: "Error", + Color: 0xff0000, + Description: msg, + } + SendEmbedToDiscord(&embed) +} diff --git a/hn.go b/hn.go index 3e2f961..2b60056 100644 --- a/hn.go +++ b/hn.go @@ -27,21 +27,23 @@ var ( HackerNewsLinkRegexp = regexp.MustCompile(`(?:https?:\/\/)?news\.ycombinator\.com\/item\?id=([0-9]+)`) ) -func FetchHackerNewsTopStories() []Story { - // API docs: https://github.com/HackerNews/API +func FetchHackerNewsTopStories() ([]Story, error) { + log.Println("Fetching HN top stories ...") + // API docs: https://github.com/HackerNews/API url := fmt.Sprintf("%s/topstories.json", HackerNewsFirebaseUrl) resp, err := http.Get(url) if err != nil { - log.Fatal("Error fetching top stories:", err) + err = fmt.Errorf("error fetching HN top stories %w:", err) + return nil, err } defer resp.Body.Close() - log.Printf("GET %s %d\n", url, resp.StatusCode) var ids []int err = json.NewDecoder(resp.Body).Decode(&ids) if err != nil { - log.Fatal("Error decoding top stories JSON:", err) + err = fmt.Errorf("error decoding HN top stories JSON: %w", err) + return nil, err } // we are only interested in the first page of top stories @@ -50,30 +52,38 @@ func FetchHackerNewsTopStories() []Story { var stories [limit]Story for i, id := range ids { - story := FetchStoryById(id) + story, err := FetchStoryById(id) + if err != nil { + return nil, err + } stories[i] = story } + log.Println("Fetching HN top stories ... OK") // Can't return [30]Story as []Story so we copy the array - return stories[:] + return stories[:], nil } -func FetchStoryById(id int) Story { +func FetchStoryById(id int) (Story, error) { + log.Printf("Fetching HN story (id=%d) ...\n", id) + url := fmt.Sprintf("https://hacker-news.firebaseio.com/v0/item/%d.json", id) resp, err := http.Get(url) if err != nil { - log.Fatal("Error fetching story:", err) + err = fmt.Errorf("error fetching HN story (id=%d): %w", id, err) + return Story{}, err } defer resp.Body.Close() - log.Printf("GET %s %d\n", url, resp.StatusCode) var story Story err = json.NewDecoder(resp.Body).Decode(&story) if err != nil { - log.Fatal("Error decoding story JSON:", err) + err := fmt.Errorf("error decoding HN story JSON (id=%d): %w", id, err) + return Story{}, err } - return story + log.Printf("Fetching HN story (id=%d) ... OK\n", id) + return story, nil } func ParseHackerNewsLink(link string) (int, error) { @@ -83,8 +93,7 @@ func ParseHackerNewsLink(link string) (int, error) { } id, err := strconv.Atoi(match[1]) if err != nil { - // this should never happen - panic(err) + return -1, errors.New("integer conversion to string failed") } return id, nil } diff --git a/main.go b/main.go index ef4d0e9..dca847f 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,35 @@ package main -import "time" +import ( + "errors" + "log" + "time" +) func main() { for { - stories := FetchHackerNewsTopStories() + + stories, err := FetchHackerNewsTopStories() + if err != nil { + SendErrorToDiscord(err) + time.Sleep(time.Hour) + continue + } + filtered := CurateContentForStackerNews(&stories) + for _, story := range *filtered { - PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false}) + _, err := PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false}) + if err != nil { + var dupesErr *DupesError + if errors.As(err, &dupesErr) { + SendDupesErrorToDiscord(story.ID, dupesErr) + continue + } + SendErrorToDiscord(err) + continue + } + log.Println("Posting to SN ... OK") } time.Sleep(time.Hour) } diff --git a/sn.go b/sn.go index d485c1f..0972bb9 100644 --- a/sn.go +++ b/sn.go @@ -45,7 +45,7 @@ type DupesError struct { } func (e *DupesError) Error() string { - return fmt.Sprintf("%s has %d dupes", e.Url, len(e.Dupes)) + return fmt.Sprintf("found %d dupes for %s", len(e.Dupes), e.Url) } type Comment struct { @@ -88,7 +88,7 @@ var ( func init() { err := godotenv.Load() if err != nil { - log.Fatal("Error loading .env file") + log.Fatal("error loading .env file") } flag.StringVar(&SnAuthCookie, "SN_AUTH_COOKIE", "", "Cookie required for authorizing requests to stacker.news/api/graphql") flag.Parse() @@ -97,15 +97,17 @@ func init() { } } -func MakeStackerNewsRequest(body GraphQLPayload) *http.Response { +func MakeStackerNewsRequest(body GraphQLPayload) (*http.Response, error) { bodyJSON, err := json.Marshal(body) if err != nil { - log.Fatal("Error during json.Marshal:", err) + err = fmt.Errorf("error encoding SN payload: %w", err) + return nil, err } req, err := http.NewRequest("POST", SnApiUrl, bytes.NewBuffer(bodyJSON)) if err != nil { - panic(err) + err = fmt.Errorf("error preparing SN request: %w", err) + return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Cookie", SnAuthCookie) @@ -113,12 +115,11 @@ func MakeStackerNewsRequest(body GraphQLPayload) *http.Response { client := http.DefaultClient resp, err := client.Do(req) if err != nil { - panic(err) + err = fmt.Errorf("error posting SN payload: %w", err) + return nil, err } - log.Printf("POST %s %d\n", SnApiUrl, resp.StatusCode) - - return resp + return resp, nil } func CurateContentForStackerNews(stories *[]Story) *[]Story { @@ -128,7 +129,9 @@ func CurateContentForStackerNews(stories *[]Story) *[]Story { return &slice } -func FetchStackerNewsDupes(url string) *[]Dupe { +func FetchStackerNewsDupes(url string) (*[]Dupe, error) { + log.Printf("Fetching SN dupes (url=%s) ...\n", url) + body := GraphQLPayload{ Query: ` query Dupes($url: String!) { @@ -148,16 +151,21 @@ func FetchStackerNewsDupes(url string) *[]Dupe { "url": url, }, } - resp := MakeStackerNewsRequest(body) + resp, err := MakeStackerNewsRequest(body) + if err != nil { + return nil, err + } defer resp.Body.Close() var dupesResp DupesResponse - err := json.NewDecoder(resp.Body).Decode(&dupesResp) + err = json.NewDecoder(resp.Body).Decode(&dupesResp) if err != nil { - log.Fatal("Error decoding dupes JSON:", err) + err = fmt.Errorf("error decoding SN dupes: %w", err) + return nil, err } - return &dupesResp.Data.Dupes + log.Printf("Fetching SN dupes (url=%s) ... OK\n", url) + return &dupesResp.Data.Dupes, nil } type PostStoryOptions struct { @@ -165,10 +173,14 @@ type PostStoryOptions struct { } func PostStoryToStackerNews(story *Story, options PostStoryOptions) (int, error) { + log.Printf("Posting to SN (url=%s) ...\n", story.Url) + if !options.SkipDupes { - dupes := FetchStackerNewsDupes(story.Url) + dupes, err := FetchStackerNewsDupes(story.Url) + if err != nil { + return -1, err + } if len(*dupes) > 0 { - log.Printf("%s was already posted. Skipping.\n", story.Url) return -1, &DupesError{story.Url, *dupes} } } @@ -185,18 +197,21 @@ func PostStoryToStackerNews(story *Story, options PostStoryOptions) (int, error) "title": story.Title, }, } - resp := MakeStackerNewsRequest(body) + resp, err := MakeStackerNewsRequest(body) + if err != nil { + return -1, err + } defer resp.Body.Close() var upsertLinkResp UpsertLinkResponse - err := json.NewDecoder(resp.Body).Decode(&upsertLinkResp) + err = json.NewDecoder(resp.Body).Decode(&upsertLinkResp) if err != nil { - log.Fatal("Error decoding dupes JSON:", err) + err = fmt.Errorf("error decoding SN upsertLink: %w", err) + return -1, err } parentId := upsertLinkResp.Data.UpsertLink.Id - log.Println("Created new post on SN") - log.Printf("id=%d title='%s' url=%s\n", parentId, story.Title, story.Url) + log.Printf("Posting to SN (url=%s) ... OK \n", story.Url) SendStackerNewsEmbedToDiscord(story.Title, parentId) comment := fmt.Sprintf( @@ -215,7 +230,9 @@ func StackerNewsItemLink(id int) string { return fmt.Sprintf("https://stacker.news/items/%d", id) } -func CommentStackerNewsPost(text string, parentId int) { +func CommentStackerNewsPost(text string, parentId int) (*http.Response, error) { + log.Printf("Commenting SN post (parentId=%d) ...\n", parentId) + body := GraphQLPayload{ Query: ` mutation createComment($text: String!, $parentId: ID!) { @@ -228,11 +245,14 @@ func CommentStackerNewsPost(text string, parentId int) { "parentId": parentId, }, } - resp := MakeStackerNewsRequest(body) + resp, err := MakeStackerNewsRequest(body) + if err != nil { + return nil, err + } defer resp.Body.Close() - log.Println("Commented post on SN") - log.Printf("text='%s' parentId=%d\n", text, parentId) + log.Printf("Commenting SN post (parentId=%d) ... OK\n", parentId) + return resp, nil } func SendStackerNewsEmbedToDiscord(title string, id int) { diff --git a/sn_test.go b/sn_test.go index 806b53d..0fe68dd 100644 --- a/sn_test.go +++ b/sn_test.go @@ -1,6 +1,7 @@ package main import ( + "log" "testing" "github.com/stretchr/testify/assert" @@ -9,6 +10,9 @@ import ( func TestFetchDupes(t *testing.T) { // TODO: mock HTTP request url := "https://en.wikipedia.org/wiki/Dishwasher_salmon" - dupes := FetchStackerNewsDupes(url) + dupes, err := FetchStackerNewsDupes(url) + if err != nil { + log.Fatal(err) + } assert.NotEmpty(t, *dupes, "Expected at least one duplicate") }