Overhaul logging and error handling

This commit is contained in:
ekzyis 2023-04-25 11:51:12 +02:00
parent 85fa5997dd
commit 5388480f31
5 changed files with 126 additions and 51 deletions

View File

@ -54,7 +54,6 @@ func initBot() {
} }
func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
if m.Author.ID == s.State.User.ID { if m.Author.ID == s.State.User.ID {
return return
} }
@ -62,15 +61,15 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
if err != nil { if err != nil {
return return
} }
story := FetchStoryById(hackerNewsId) story, err := FetchStoryById(hackerNewsId)
_, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false}) _, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false})
if err != nil { if err != nil {
var dupesErr *DupesError var dupesErr *DupesError
if errors.As(err, &dupesErr) { if errors.As(err, &dupesErr) {
SendDupesErrorToDiscord(hackerNewsId, dupesErr) SendDupesErrorToDiscord(hackerNewsId, dupesErr)
} else { return
log.Fatal("unexpected error returned")
} }
SendErrorToDiscord(err)
} }
} }
@ -83,7 +82,7 @@ func onMessageReact(s *discordgo.Session, reaction *discordgo.MessageReactionAdd
} }
m, err := s.ChannelMessage(reaction.ChannelID, reaction.MessageID) m, err := s.ChannelMessage(reaction.ChannelID, reaction.MessageID)
if err != nil { if err != nil {
log.Println("error:", err) SendErrorToDiscord(err)
return return
} }
if len(m.Embeds) == 0 { if len(m.Embeds) == 0 {
@ -97,14 +96,21 @@ func onMessageReact(s *discordgo.Session, reaction *discordgo.MessageReactionAdd
if err != nil { if err != nil {
return return
} }
story := FetchStoryById(id) story, err := FetchStoryById(id)
if err != nil {
SendErrorToDiscord(err)
return
}
id, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: true}) id, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: true})
if err != nil { if err != nil {
log.Fatal("unexpected error returned") SendErrorToDiscord(err)
} }
} }
func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) { 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) title := fmt.Sprintf("%d dupe(s) found for %s:", len(dupesErr.Dupes), dupesErr.Url)
color := 0xffc107 color := 0xffc107
var fields []*discordgo.MessageEmbedField var fields []*discordgo.MessageEmbedField
@ -147,6 +153,7 @@ func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) {
}, },
) )
} }
embed := discordgo.MessageEmbed{ embed := discordgo.MessageEmbed{
Title: title, Title: title,
Color: color, Color: color,
@ -162,6 +169,19 @@ func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) {
func SendEmbedToDiscord(embed *discordgo.MessageEmbed) { func SendEmbedToDiscord(embed *discordgo.MessageEmbed) {
_, err := dg.ChannelMessageSendEmbed(DiscordChannelId, embed) _, err := dg.ChannelMessageSendEmbed(DiscordChannelId, embed)
if err != nil { 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)
}

37
hn.go
View File

@ -27,21 +27,23 @@ var (
HackerNewsLinkRegexp = regexp.MustCompile(`(?:https?:\/\/)?news\.ycombinator\.com\/item\?id=([0-9]+)`) HackerNewsLinkRegexp = regexp.MustCompile(`(?:https?:\/\/)?news\.ycombinator\.com\/item\?id=([0-9]+)`)
) )
func FetchHackerNewsTopStories() []Story { func FetchHackerNewsTopStories() ([]Story, error) {
// API docs: https://github.com/HackerNews/API log.Println("Fetching HN top stories ...")
// API docs: https://github.com/HackerNews/API
url := fmt.Sprintf("%s/topstories.json", HackerNewsFirebaseUrl) url := fmt.Sprintf("%s/topstories.json", HackerNewsFirebaseUrl)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { 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() defer resp.Body.Close()
log.Printf("GET %s %d\n", url, resp.StatusCode)
var ids []int var ids []int
err = json.NewDecoder(resp.Body).Decode(&ids) err = json.NewDecoder(resp.Body).Decode(&ids)
if err != nil { 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 // we are only interested in the first page of top stories
@ -50,30 +52,38 @@ func FetchHackerNewsTopStories() []Story {
var stories [limit]Story var stories [limit]Story
for i, id := range ids { for i, id := range ids {
story := FetchStoryById(id) story, err := FetchStoryById(id)
if err != nil {
return nil, err
}
stories[i] = story stories[i] = story
} }
log.Println("Fetching HN top stories ... OK")
// Can't return [30]Story as []Story so we copy the array // 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) url := fmt.Sprintf("https://hacker-news.firebaseio.com/v0/item/%d.json", id)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { 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() defer resp.Body.Close()
log.Printf("GET %s %d\n", url, resp.StatusCode)
var story Story var story Story
err = json.NewDecoder(resp.Body).Decode(&story) err = json.NewDecoder(resp.Body).Decode(&story)
if err != nil { 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) { func ParseHackerNewsLink(link string) (int, error) {
@ -83,8 +93,7 @@ func ParseHackerNewsLink(link string) (int, error) {
} }
id, err := strconv.Atoi(match[1]) id, err := strconv.Atoi(match[1])
if err != nil { if err != nil {
// this should never happen return -1, errors.New("integer conversion to string failed")
panic(err)
} }
return id, nil return id, nil
} }

28
main.go
View File

@ -1,13 +1,35 @@
package main package main
import "time" import (
"errors"
"log"
"time"
)
func main() { func main() {
for { for {
stories := FetchHackerNewsTopStories()
stories, err := FetchHackerNewsTopStories()
if err != nil {
SendErrorToDiscord(err)
time.Sleep(time.Hour)
continue
}
filtered := CurateContentForStackerNews(&stories) filtered := CurateContentForStackerNews(&stories)
for _, story := range *filtered { 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) time.Sleep(time.Hour)
} }

70
sn.go
View File

@ -45,7 +45,7 @@ type DupesError struct {
} }
func (e *DupesError) Error() string { 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 { type Comment struct {
@ -88,7 +88,7 @@ var (
func init() { func init() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { 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.StringVar(&SnAuthCookie, "SN_AUTH_COOKIE", "", "Cookie required for authorizing requests to stacker.news/api/graphql")
flag.Parse() 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) bodyJSON, err := json.Marshal(body)
if err != nil { 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)) req, err := http.NewRequest("POST", SnApiUrl, bytes.NewBuffer(bodyJSON))
if err != nil { 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("Content-Type", "application/json")
req.Header.Set("Cookie", SnAuthCookie) req.Header.Set("Cookie", SnAuthCookie)
@ -113,12 +115,11 @@ func MakeStackerNewsRequest(body GraphQLPayload) *http.Response {
client := http.DefaultClient client := http.DefaultClient
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { 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, nil
return resp
} }
func CurateContentForStackerNews(stories *[]Story) *[]Story { func CurateContentForStackerNews(stories *[]Story) *[]Story {
@ -128,7 +129,9 @@ func CurateContentForStackerNews(stories *[]Story) *[]Story {
return &slice return &slice
} }
func FetchStackerNewsDupes(url string) *[]Dupe { func FetchStackerNewsDupes(url string) (*[]Dupe, error) {
log.Printf("Fetching SN dupes (url=%s) ...\n", url)
body := GraphQLPayload{ body := GraphQLPayload{
Query: ` Query: `
query Dupes($url: String!) { query Dupes($url: String!) {
@ -148,16 +151,21 @@ func FetchStackerNewsDupes(url string) *[]Dupe {
"url": url, "url": url,
}, },
} }
resp := MakeStackerNewsRequest(body) resp, err := MakeStackerNewsRequest(body)
if err != nil {
return nil, err
}
defer resp.Body.Close() defer resp.Body.Close()
var dupesResp DupesResponse var dupesResp DupesResponse
err := json.NewDecoder(resp.Body).Decode(&dupesResp) err = json.NewDecoder(resp.Body).Decode(&dupesResp)
if err != nil { 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 { type PostStoryOptions struct {
@ -165,10 +173,14 @@ type PostStoryOptions struct {
} }
func PostStoryToStackerNews(story *Story, options PostStoryOptions) (int, error) { func PostStoryToStackerNews(story *Story, options PostStoryOptions) (int, error) {
log.Printf("Posting to SN (url=%s) ...\n", story.Url)
if !options.SkipDupes { if !options.SkipDupes {
dupes := FetchStackerNewsDupes(story.Url) dupes, err := FetchStackerNewsDupes(story.Url)
if err != nil {
return -1, err
}
if len(*dupes) > 0 { if len(*dupes) > 0 {
log.Printf("%s was already posted. Skipping.\n", story.Url)
return -1, &DupesError{story.Url, *dupes} return -1, &DupesError{story.Url, *dupes}
} }
} }
@ -185,18 +197,21 @@ func PostStoryToStackerNews(story *Story, options PostStoryOptions) (int, error)
"title": story.Title, "title": story.Title,
}, },
} }
resp := MakeStackerNewsRequest(body) resp, err := MakeStackerNewsRequest(body)
if err != nil {
return -1, err
}
defer resp.Body.Close() defer resp.Body.Close()
var upsertLinkResp UpsertLinkResponse var upsertLinkResp UpsertLinkResponse
err := json.NewDecoder(resp.Body).Decode(&upsertLinkResp) err = json.NewDecoder(resp.Body).Decode(&upsertLinkResp)
if err != nil { 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 parentId := upsertLinkResp.Data.UpsertLink.Id
log.Println("Created new post on SN") log.Printf("Posting to SN (url=%s) ... OK \n", story.Url)
log.Printf("id=%d title='%s' url=%s\n", parentId, story.Title, story.Url)
SendStackerNewsEmbedToDiscord(story.Title, parentId) SendStackerNewsEmbedToDiscord(story.Title, parentId)
comment := fmt.Sprintf( comment := fmt.Sprintf(
@ -215,7 +230,9 @@ func StackerNewsItemLink(id int) string {
return fmt.Sprintf("https://stacker.news/items/%d", id) 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{ body := GraphQLPayload{
Query: ` Query: `
mutation createComment($text: String!, $parentId: ID!) { mutation createComment($text: String!, $parentId: ID!) {
@ -228,11 +245,14 @@ func CommentStackerNewsPost(text string, parentId int) {
"parentId": parentId, "parentId": parentId,
}, },
} }
resp := MakeStackerNewsRequest(body) resp, err := MakeStackerNewsRequest(body)
if err != nil {
return nil, err
}
defer resp.Body.Close() defer resp.Body.Close()
log.Println("Commented post on SN") log.Printf("Commenting SN post (parentId=%d) ... OK\n", parentId)
log.Printf("text='%s' parentId=%d\n", text, parentId) return resp, nil
} }
func SendStackerNewsEmbedToDiscord(title string, id int) { func SendStackerNewsEmbedToDiscord(title string, id int) {

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -9,6 +10,9 @@ import (
func TestFetchDupes(t *testing.T) { func TestFetchDupes(t *testing.T) {
// TODO: mock HTTP request // TODO: mock HTTP request
url := "https://en.wikipedia.org/wiki/Dishwasher_salmon" 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") assert.NotEmpty(t, *dupes, "Expected at least one duplicate")
} }