From 6010e47dde3e698549595cbbab1502c3793642dd Mon Sep 17 00:00:00 2001 From: ekzyis Date: Tue, 25 Apr 2023 00:42:11 +0200 Subject: [PATCH 1/4] Show dupes in discord --- discord.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++---- sn.go | 33 ++++++++++++++++++---- 2 files changed, 103 insertions(+), 11 deletions(-) diff --git a/discord.go b/discord.go index 1ac7599..d7ebc1a 100644 --- a/discord.go +++ b/discord.go @@ -3,10 +3,13 @@ package main import ( "bytes" "encoding/json" + "errors" + "fmt" "log" "net/http" "github.com/bwmarrin/discordgo" + "github.com/dustin/go-humanize" "github.com/joho/godotenv" "github.com/namsral/flag" ) @@ -22,12 +25,19 @@ type DiscordEmbedFooter struct { IconUrl string `json:"icon_url"` } +type DiscordEmbedField struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline"` +} + type DiscordEmbed struct { - Title string `json:"title"` - Url string `json:"url"` - Color int `json:"color"` - Footer DiscordEmbedFooter `json:"footer"` - Timestamp string `json:"timestamp"` + Title string `json:"title"` + Url string `json:"url"` + Color int `json:"color"` + Footer DiscordEmbedFooter `json:"footer"` + Timestamp string `json:"timestamp"` + Fields []DiscordEmbedField `json:"fields"` } type DiscordWebhookPayload struct { @@ -78,7 +88,66 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { return } story := FetchStoryById(id) - PostStoryToStackerNews(&story) + id, err = PostStoryToStackerNews(&story) + if err != nil { + var dupesErr *DupesError + if errors.As(err, &dupesErr) { + SendDupesErrorToDiscord(dupesErr) + } else { + log.Fatal("unexpected error returned") + } + } +} + +func SendDupesErrorToDiscord(dupesErr *DupesError) { + title := fmt.Sprintf("%d dupe(s) found for %s:", len(dupesErr.Dupes), dupesErr.Url) + color := 0xffc107 + var fields []DiscordEmbedField + for _, dupe := range dupesErr.Dupes { + fields = append(fields, + DiscordEmbedField{ + Name: "Title", + Value: dupe.Title, + Inline: false, + }, + DiscordEmbedField{ + Name: "Id", + Value: StackerNewsItemLink(dupe.Id), + Inline: true, + }, + DiscordEmbedField{ + Name: "Url", + Value: dupe.Url, + Inline: true, + }, + DiscordEmbedField{ + Name: "User", + Value: dupe.User.Name, + Inline: true, + }, + DiscordEmbedField{ + Name: "Created", + Value: humanize.Time(dupe.CreatedAt), + Inline: true, + }, + DiscordEmbedField{ + Name: "Sats", + Value: fmt.Sprint(dupe.Sats), + Inline: true, + }, + DiscordEmbedField{ + Name: "Comments", + Value: fmt.Sprint(dupe.NComments), + Inline: true, + }, + ) + } + embed := DiscordEmbed{ + Title: title, + Color: color, + Fields: fields, + } + SendEmbedToDiscord(embed) } func SendEmbedToDiscord(embed DiscordEmbed) { diff --git a/sn.go b/sn.go index f3b624e..e080067 100644 --- a/sn.go +++ b/sn.go @@ -18,10 +18,17 @@ type GraphQLPayload struct { Variables map[string]interface{} `json:"variables,omitempty"` } +type SnUser struct { + Name string `json:"name"` +} type Dupe struct { - Id int `json:"id,string"` - Url string `json:"url"` - Title string `json:"title"` + Id int `json:"id,string"` + Url string `json:"url"` + Title string `json:"title"` + User SnUser `json:"user"` + CreatedAt time.Time `json:"createdAt"` + Sats int `json:"sats"` + NComments int `json:"ncomments"` } type DupesResponse struct { @@ -30,6 +37,15 @@ type DupesResponse struct { } `json:"data"` } +type DupesError struct { + Url string + Dupes []Dupe +} + +func (e *DupesError) Error() string { + return fmt.Sprintf("%s has %d dupes", e.Url, len(e.Dupes)) +} + type User struct { Name string `json:"name"` } @@ -123,6 +139,12 @@ func FetchStackerNewsDupes(url string) *[]Dupe { id url title + user { + name + } + createdAt + sats + ncomments } }`, Variables: map[string]interface{}{ @@ -141,11 +163,11 @@ func FetchStackerNewsDupes(url string) *[]Dupe { return &dupesResp.Data.Dupes } -func PostStoryToStackerNews(story *Story) { +func PostStoryToStackerNews(story *Story) (int, error) { dupes := FetchStackerNewsDupes(story.Url) if len(*dupes) > 0 { log.Printf("%s was already posted. Skipping.\n", story.Url) - return + return -1, &DupesError{story.Url, *dupes} } body := GraphQLPayload{ @@ -183,6 +205,7 @@ func PostStoryToStackerNews(story *Story) { story.Score, story.Descendants, ) CommentStackerNewsPost(comment, parentId) + return parentId, nil } func StackerNewsItemLink(id int) string { From b2b957e5c35539a65677696eeaee84ec3ce2233b Mon Sep 17 00:00:00 2001 From: ekzyis Date: Tue, 25 Apr 2023 01:57:37 +0200 Subject: [PATCH 2/4] Replace webhook with discordgo --- .env.template | 2 +- discord.go | 79 +++++++++++++-------------------------------------- sn.go | 11 +++---- 3 files changed, 27 insertions(+), 65 deletions(-) diff --git a/.env.template b/.env.template index b07ca8a..6954647 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,3 @@ SN_AUTH_COOKIE= -DISCORD_WEBHOOK= DISCORD_TOKEN= +DISCORD_CHANNEL_ID= diff --git a/discord.go b/discord.go index d7ebc1a..a20cd1c 100644 --- a/discord.go +++ b/discord.go @@ -1,12 +1,9 @@ package main import ( - "bytes" - "encoding/json" "errors" "fmt" "log" - "net/http" "github.com/bwmarrin/discordgo" "github.com/dustin/go-humanize" @@ -15,55 +12,31 @@ import ( ) var ( - DiscordWebhook string - DiscordToken string - DiscordClient *discordgo.Session + DiscordToken string + DiscordClient *discordgo.Session + DiscordChannelId string ) -type DiscordEmbedFooter struct { - Text string `json:"text"` - IconUrl string `json:"icon_url"` -} - -type DiscordEmbedField struct { - Name string `json:"name"` - Value string `json:"value"` - Inline bool `json:"inline"` -} - -type DiscordEmbed struct { - Title string `json:"title"` - Url string `json:"url"` - Color int `json:"color"` - Footer DiscordEmbedFooter `json:"footer"` - Timestamp string `json:"timestamp"` - Fields []DiscordEmbedField `json:"fields"` -} - -type DiscordWebhookPayload struct { - Embeds []DiscordEmbed `json:"embeds"` -} - func init() { err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file") } - flag.StringVar(&DiscordWebhook, "DISCORD_WEBHOOK", "", "Webhook to send logs to discord") flag.StringVar(&DiscordToken, "DISCORD_TOKEN", "", "Discord bot token") + flag.StringVar(&DiscordChannelId, "DISCORD_CHANNEL_ID", "", "Discord channel id") flag.Parse() - if DiscordWebhook == "" { - log.Fatal("DISCORD_WEBHOOK not set") - } if DiscordToken == "" { log.Fatal("DISCORD_TOKEN not set") } + if DiscordChannelId == "" { + log.Fatal("DISCORD_CHANNEL_ID not set") + } initBot() } func initBot() { var err error - DiscordClient, err = discordgo.New(DiscordToken) + DiscordClient, err = discordgo.New("Bot " + DiscordToken) if err != nil { log.Fatal("error creating discord session:", err) } @@ -102,69 +75,57 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { func SendDupesErrorToDiscord(dupesErr *DupesError) { title := fmt.Sprintf("%d dupe(s) found for %s:", len(dupesErr.Dupes), dupesErr.Url) color := 0xffc107 - var fields []DiscordEmbedField + var fields []*discordgo.MessageEmbedField for _, dupe := range dupesErr.Dupes { fields = append(fields, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "Title", Value: dupe.Title, Inline: false, }, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "Id", Value: StackerNewsItemLink(dupe.Id), Inline: true, }, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "Url", Value: dupe.Url, Inline: true, }, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "User", Value: dupe.User.Name, Inline: true, }, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "Created", Value: humanize.Time(dupe.CreatedAt), Inline: true, }, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "Sats", Value: fmt.Sprint(dupe.Sats), Inline: true, }, - DiscordEmbedField{ + &discordgo.MessageEmbedField{ Name: "Comments", Value: fmt.Sprint(dupe.NComments), Inline: true, }, ) } - embed := DiscordEmbed{ + embed := discordgo.MessageEmbed{ Title: title, Color: color, Fields: fields, } - SendEmbedToDiscord(embed) + SendEmbedToDiscord(&embed) } -func SendEmbedToDiscord(embed DiscordEmbed) { - bodyJSON, err := json.Marshal( - DiscordWebhookPayload{ - Embeds: []DiscordEmbed{embed}, - }, - ) +func SendEmbedToDiscord(embed *discordgo.MessageEmbed) { + _, err := DiscordClient.ChannelMessageSendEmbed(DiscordChannelId, embed) if err != nil { log.Fatal("Error during json.Marshal:", err) } - req, err := http.NewRequest("POST", DiscordWebhook, bytes.NewBuffer(bodyJSON)) - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - log.Println("Discord webhook error:", err) - } - defer resp.Body.Close() } diff --git a/sn.go b/sn.go index e080067..58ab0ce 100644 --- a/sn.go +++ b/sn.go @@ -8,6 +8,7 @@ import ( "net/http" "time" + "github.com/bwmarrin/discordgo" "github.com/dustin/go-humanize" "github.com/joho/godotenv" "github.com/namsral/flag" @@ -236,15 +237,15 @@ func SendStackerNewsEmbedToDiscord(title string, id int) { Timestamp := time.Now().Format(time.RFC3339) url := StackerNewsItemLink(id) color := 0xffc107 - embed := DiscordEmbed{ + embed := discordgo.MessageEmbed{ Title: title, - Url: url, + URL: url, Color: color, - Footer: DiscordEmbedFooter{ + Footer: &discordgo.MessageEmbedFooter{ Text: "Stacker News", - IconUrl: "https://stacker.news/favicon.png", + IconURL: "https://stacker.news/favicon.png", }, Timestamp: Timestamp, } - SendEmbedToDiscord(embed) + SendEmbedToDiscord(&embed) } From 8eaaaeab3e60101d313e273421f6eabd4492283d Mon Sep 17 00:00:00 2001 From: ekzyis Date: Tue, 25 Apr 2023 02:01:58 +0200 Subject: [PATCH 3/4] Rename var DiscordClient to dg --- discord.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/discord.go b/discord.go index a20cd1c..d97fcad 100644 --- a/discord.go +++ b/discord.go @@ -13,7 +13,7 @@ import ( var ( DiscordToken string - DiscordClient *discordgo.Session + dg *discordgo.Session DiscordChannelId string ) @@ -36,16 +36,16 @@ func init() { func initBot() { var err error - DiscordClient, err = discordgo.New("Bot " + DiscordToken) + dg, err = discordgo.New("Bot " + DiscordToken) if err != nil { log.Fatal("error creating discord session:", err) } - DiscordClient.AddHandler(func(s *discordgo.Session, event *discordgo.Ready) { + dg.AddHandler(func(s *discordgo.Session, event *discordgo.Ready) { log.Println("Logged in as", event.User.Username) }) - DiscordClient.AddHandler(onMessage) - DiscordClient.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent - err = DiscordClient.Open() + dg.AddHandler(onMessage) + dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent + err = dg.Open() if err != nil { log.Fatal("error opening connection to discord: ", err, " -- Is your token correct?") } @@ -124,7 +124,7 @@ func SendDupesErrorToDiscord(dupesErr *DupesError) { } func SendEmbedToDiscord(embed *discordgo.MessageEmbed) { - _, err := DiscordClient.ChannelMessageSendEmbed(DiscordChannelId, embed) + _, err := dg.ChannelMessageSendEmbed(DiscordChannelId, embed) if err != nil { log.Fatal("Error during json.Marshal:", err) } From c2b6e777510c85190b58d2cde4e7f204a16c174a Mon Sep 17 00:00:00 2001 From: ekzyis Date: Tue, 25 Apr 2023 02:22:27 +0200 Subject: [PATCH 4/4] Skip dupes check on skip reaction --- discord.go | 48 ++++++++++++++++++++++++++++++++++++++++++------ main.go | 2 +- sn.go | 16 +++++++++++----- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/discord.go b/discord.go index d97fcad..b4ed143 100644 --- a/discord.go +++ b/discord.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "strings" "github.com/bwmarrin/discordgo" "github.com/dustin/go-humanize" @@ -44,7 +45,8 @@ func initBot() { log.Println("Logged in as", event.User.Username) }) dg.AddHandler(onMessage) - dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent + dg.AddHandler(onMessageReact) + dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent | discordgo.IntentGuildMessageReactions err = dg.Open() if err != nil { log.Fatal("error opening connection to discord: ", err, " -- Is your token correct?") @@ -56,23 +58,53 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { if m.Author.ID == s.State.User.ID { return } - id, err := ParseHackerNewsLink(m.Content) + hackerNewsId, err := ParseHackerNewsLink(m.Content) if err != nil { return } - story := FetchStoryById(id) - id, err = PostStoryToStackerNews(&story) + story := FetchStoryById(hackerNewsId) + _, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false}) if err != nil { var dupesErr *DupesError if errors.As(err, &dupesErr) { - SendDupesErrorToDiscord(dupesErr) + SendDupesErrorToDiscord(hackerNewsId, dupesErr) } else { log.Fatal("unexpected error returned") } } } -func SendDupesErrorToDiscord(dupesErr *DupesError) { +func onMessageReact(s *discordgo.Session, reaction *discordgo.MessageReactionAdd) { + if reaction.UserID == s.State.User.ID { + return + } + if reaction.Emoji.Name != "⏭️" { + return + } + m, err := s.ChannelMessage(reaction.ChannelID, reaction.MessageID) + if err != nil { + log.Println("error:", err) + return + } + if len(m.Embeds) == 0 { + return + } + embed := m.Embeds[0] + if !strings.Contains(embed.Title, "dupe(s) found for") { + return + } + id, err := ParseHackerNewsLink(embed.Footer.Text) + if err != nil { + return + } + story := FetchStoryById(id) + id, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: true}) + if err != nil { + log.Fatal("unexpected error returned") + } +} + +func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) { title := fmt.Sprintf("%d dupe(s) found for %s:", len(dupesErr.Dupes), dupesErr.Url) color := 0xffc107 var fields []*discordgo.MessageEmbedField @@ -119,6 +151,10 @@ func SendDupesErrorToDiscord(dupesErr *DupesError) { Title: title, Color: color, Fields: fields, + Footer: &discordgo.MessageEmbedFooter{ + Text: HackerNewsItemLink(hackerNewsId), + IconURL: "https://news.ycombinator.com/y18.gif", + }, } SendEmbedToDiscord(&embed) } diff --git a/main.go b/main.go index 4fa8ef7..ef4d0e9 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ func main() { stories := FetchHackerNewsTopStories() filtered := CurateContentForStackerNews(&stories) for _, story := range *filtered { - PostStoryToStackerNews(&story) + PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false}) } time.Sleep(time.Hour) } diff --git a/sn.go b/sn.go index 58ab0ce..b49a2e2 100644 --- a/sn.go +++ b/sn.go @@ -164,11 +164,17 @@ func FetchStackerNewsDupes(url string) *[]Dupe { return &dupesResp.Data.Dupes } -func PostStoryToStackerNews(story *Story) (int, error) { - dupes := FetchStackerNewsDupes(story.Url) - if len(*dupes) > 0 { - log.Printf("%s was already posted. Skipping.\n", story.Url) - return -1, &DupesError{story.Url, *dupes} +type PostStoryOptions struct { + SkipDupes bool +} + +func PostStoryToStackerNews(story *Story, options PostStoryOptions) (int, error) { + if !options.SkipDupes { + dupes := FetchStackerNewsDupes(story.Url) + if len(*dupes) > 0 { + log.Printf("%s was already posted. Skipping.\n", story.Url) + return -1, &DupesError{story.Url, *dupes} + } } body := GraphQLPayload{