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) {
// 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)
}

37
hn.go
View File

@ -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
}

28
main.go
View File

@ -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)
}

70
sn.go
View File

@ -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) {

View File

@ -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")
}