Merge branch '6-show-error-if-dupes-exist-and-add-option-to-override' into 'develop'
Resolve "Show error if dupes exist and add option to override" Closes #6 See merge request ekzyis/hnbot!5
This commit is contained in:
commit
26aa14c9a1
|
@ -1,3 +1,3 @@
|
||||||
SN_AUTH_COOKIE=
|
SN_AUTH_COOKIE=
|
||||||
DISCORD_WEBHOOK=
|
|
||||||
DISCORD_TOKEN=
|
DISCORD_TOKEN=
|
||||||
|
DISCORD_CHANNEL_ID=
|
||||||
|
|
160
discord.go
160
discord.go
|
@ -1,68 +1,53 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"errors"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"strings"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/namsral/flag"
|
"github.com/namsral/flag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DiscordWebhook string
|
DiscordToken string
|
||||||
DiscordToken string
|
dg *discordgo.Session
|
||||||
DiscordClient *discordgo.Session
|
DiscordChannelId string
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscordEmbedFooter struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
IconUrl string `json:"icon_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscordEmbed struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Color int `json:"color"`
|
|
||||||
Footer DiscordEmbedFooter `json:"footer"`
|
|
||||||
Timestamp string `json:"timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscordWebhookPayload struct {
|
|
||||||
Embeds []DiscordEmbed `json:"embeds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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(&DiscordWebhook, "DISCORD_WEBHOOK", "", "Webhook to send logs to discord")
|
|
||||||
flag.StringVar(&DiscordToken, "DISCORD_TOKEN", "", "Discord bot token")
|
flag.StringVar(&DiscordToken, "DISCORD_TOKEN", "", "Discord bot token")
|
||||||
|
flag.StringVar(&DiscordChannelId, "DISCORD_CHANNEL_ID", "", "Discord channel id")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if DiscordWebhook == "" {
|
|
||||||
log.Fatal("DISCORD_WEBHOOK not set")
|
|
||||||
}
|
|
||||||
if DiscordToken == "" {
|
if DiscordToken == "" {
|
||||||
log.Fatal("DISCORD_TOKEN not set")
|
log.Fatal("DISCORD_TOKEN not set")
|
||||||
}
|
}
|
||||||
|
if DiscordChannelId == "" {
|
||||||
|
log.Fatal("DISCORD_CHANNEL_ID not set")
|
||||||
|
}
|
||||||
initBot()
|
initBot()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBot() {
|
func initBot() {
|
||||||
var err error
|
var err error
|
||||||
DiscordClient, err = discordgo.New(DiscordToken)
|
dg, err = discordgo.New("Bot " + DiscordToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("error creating discord session:", err)
|
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)
|
log.Println("Logged in as", event.User.Username)
|
||||||
})
|
})
|
||||||
DiscordClient.AddHandler(onMessage)
|
dg.AddHandler(onMessage)
|
||||||
DiscordClient.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent
|
dg.AddHandler(onMessageReact)
|
||||||
err = DiscordClient.Open()
|
dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent | discordgo.IntentGuildMessageReactions
|
||||||
|
err = dg.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("error opening connection to discord: ", err, " -- Is your token correct?")
|
log.Fatal("error opening connection to discord: ", err, " -- Is your token correct?")
|
||||||
}
|
}
|
||||||
|
@ -73,29 +58,110 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
if m.Author.ID == s.State.User.ID {
|
if m.Author.ID == s.State.User.ID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id, err := ParseHackerNewsLink(m.Content)
|
hackerNewsId, err := ParseHackerNewsLink(m.Content)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
story := 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
story := FetchStoryById(id)
|
story := FetchStoryById(id)
|
||||||
PostStoryToStackerNews(&story)
|
id, err = PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: true})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("unexpected error returned")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmbedToDiscord(embed DiscordEmbed) {
|
func SendDupesErrorToDiscord(hackerNewsId int, dupesErr *DupesError) {
|
||||||
bodyJSON, err := json.Marshal(
|
title := fmt.Sprintf("%d dupe(s) found for %s:", len(dupesErr.Dupes), dupesErr.Url)
|
||||||
DiscordWebhookPayload{
|
color := 0xffc107
|
||||||
Embeds: []DiscordEmbed{embed},
|
var fields []*discordgo.MessageEmbedField
|
||||||
|
for _, dupe := range dupesErr.Dupes {
|
||||||
|
fields = append(fields,
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "Title",
|
||||||
|
Value: dupe.Title,
|
||||||
|
Inline: false,
|
||||||
|
},
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "Id",
|
||||||
|
Value: StackerNewsItemLink(dupe.Id),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "Url",
|
||||||
|
Value: dupe.Url,
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "User",
|
||||||
|
Value: dupe.User.Name,
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "Created",
|
||||||
|
Value: humanize.Time(dupe.CreatedAt),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "Sats",
|
||||||
|
Value: fmt.Sprint(dupe.Sats),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
&discordgo.MessageEmbedField{
|
||||||
|
Name: "Comments",
|
||||||
|
Value: fmt.Sprint(dupe.NComments),
|
||||||
|
Inline: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
embed := discordgo.MessageEmbed{
|
||||||
|
Title: title,
|
||||||
|
Color: color,
|
||||||
|
Fields: fields,
|
||||||
|
Footer: &discordgo.MessageEmbedFooter{
|
||||||
|
Text: HackerNewsItemLink(hackerNewsId),
|
||||||
|
IconURL: "https://news.ycombinator.com/y18.gif",
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
|
SendEmbedToDiscord(&embed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendEmbedToDiscord(embed *discordgo.MessageEmbed) {
|
||||||
|
_, err := dg.ChannelMessageSendEmbed(DiscordChannelId, embed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error during json.Marshal:", err)
|
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()
|
|
||||||
}
|
}
|
||||||
|
|
2
main.go
2
main.go
|
@ -7,7 +7,7 @@ func main() {
|
||||||
stories := FetchHackerNewsTopStories()
|
stories := FetchHackerNewsTopStories()
|
||||||
filtered := CurateContentForStackerNews(&stories)
|
filtered := CurateContentForStackerNews(&stories)
|
||||||
for _, story := range *filtered {
|
for _, story := range *filtered {
|
||||||
PostStoryToStackerNews(&story)
|
PostStoryToStackerNews(&story, PostStoryOptions{SkipDupes: false})
|
||||||
}
|
}
|
||||||
time.Sleep(time.Hour)
|
time.Sleep(time.Hour)
|
||||||
}
|
}
|
||||||
|
|
56
sn.go
56
sn.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/namsral/flag"
|
"github.com/namsral/flag"
|
||||||
|
@ -18,10 +19,17 @@ type GraphQLPayload struct {
|
||||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SnUser struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
type Dupe struct {
|
type Dupe struct {
|
||||||
Id int `json:"id,string"`
|
Id int `json:"id,string"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
User SnUser `json:"user"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Sats int `json:"sats"`
|
||||||
|
NComments int `json:"ncomments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DupesResponse struct {
|
type DupesResponse struct {
|
||||||
|
@ -30,6 +38,15 @@ type DupesResponse struct {
|
||||||
} `json:"data"`
|
} `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 {
|
type User struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
@ -123,6 +140,12 @@ func FetchStackerNewsDupes(url string) *[]Dupe {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
title
|
title
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
sats
|
||||||
|
ncomments
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
Variables: map[string]interface{}{
|
Variables: map[string]interface{}{
|
||||||
|
@ -141,11 +164,17 @@ func FetchStackerNewsDupes(url string) *[]Dupe {
|
||||||
return &dupesResp.Data.Dupes
|
return &dupesResp.Data.Dupes
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostStoryToStackerNews(story *Story) {
|
type PostStoryOptions struct {
|
||||||
dupes := FetchStackerNewsDupes(story.Url)
|
SkipDupes bool
|
||||||
if len(*dupes) > 0 {
|
}
|
||||||
log.Printf("%s was already posted. Skipping.\n", story.Url)
|
|
||||||
return
|
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{
|
body := GraphQLPayload{
|
||||||
|
@ -183,6 +212,7 @@ func PostStoryToStackerNews(story *Story) {
|
||||||
story.Score, story.Descendants,
|
story.Score, story.Descendants,
|
||||||
)
|
)
|
||||||
CommentStackerNewsPost(comment, parentId)
|
CommentStackerNewsPost(comment, parentId)
|
||||||
|
return parentId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StackerNewsItemLink(id int) string {
|
func StackerNewsItemLink(id int) string {
|
||||||
|
@ -213,15 +243,15 @@ func SendStackerNewsEmbedToDiscord(title string, id int) {
|
||||||
Timestamp := time.Now().Format(time.RFC3339)
|
Timestamp := time.Now().Format(time.RFC3339)
|
||||||
url := StackerNewsItemLink(id)
|
url := StackerNewsItemLink(id)
|
||||||
color := 0xffc107
|
color := 0xffc107
|
||||||
embed := DiscordEmbed{
|
embed := discordgo.MessageEmbed{
|
||||||
Title: title,
|
Title: title,
|
||||||
Url: url,
|
URL: url,
|
||||||
Color: color,
|
Color: color,
|
||||||
Footer: DiscordEmbedFooter{
|
Footer: &discordgo.MessageEmbedFooter{
|
||||||
Text: "Stacker News",
|
Text: "Stacker News",
|
||||||
IconUrl: "https://stacker.news/favicon.png",
|
IconURL: "https://stacker.news/favicon.png",
|
||||||
},
|
},
|
||||||
Timestamp: Timestamp,
|
Timestamp: Timestamp,
|
||||||
}
|
}
|
||||||
SendEmbedToDiscord(embed)
|
SendEmbedToDiscord(&embed)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue