diff --git a/.env.template b/.env.template index 8b8c8ec..4ed0ca1 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,3 @@ SN_AUTH_COOKIE= +SN_USERNAME=hn HN_AUTH_COOKIE= diff --git a/hn.go b/hn.go index c42fb7b..fda1104 100644 --- a/hn.go +++ b/hn.go @@ -159,3 +159,16 @@ func HackerNewsUserLink(user string) string { func HackerNewsItemLink(id int) string { return fmt.Sprintf("%s/item?id=%d", HackerNewsUrl, id) } + +func FindHackerNewsItemId(text string) int { + re := regexp.MustCompile(fmt.Sprintf(`\[HN\]\(%s/item\?id=([0-9]+)\)`, HackerNewsUrl)) + match := re.FindStringSubmatch(text) + if len(match) == 0 { + log.Fatal("No Hacker News item URL found") + } + id, err := strconv.Atoi(match[1]) + if err != nil { + panic(err) + } + return id +} diff --git a/main.go b/main.go index 27fa5c6..5080f00 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,88 @@ package main +import ( + "fmt" + "log" + "time" + + "github.com/joho/godotenv" + "github.com/namsral/flag" +) + +var ( + SnUserName string +) + +func init() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + flag.StringVar(&SnUserName, "SN_USERNAME", "", "Username of bot on SN") + flag.Parse() + if SnUserName == "" { + log.Fatal("SN_USERNAME not set") + } +} + +func GenerateHnComment(id int, sats int, nComments int) string { + lnInvoiceDocs := "https://docs.lightning.engineering/the-lightning-network/payment-lifecycle/understanding-lightning-invoices" + return fmt.Sprintf( + ""+ + "Your post received %d sats and %d comments on %s [0].\n\n"+ + "To claim your sats, reply to this comment with a LN address or invoice [1].\n\n"+ + "You can create a SN account to obtain a LN address.\n"+ + "\n\n"+ + "[0] %s/r/%s (referral link)\n\n"+ + "[1] %s", + sats, + nComments, + StackerNewsUrl, + StackerNewsItemLink(id), + SnUserName, + lnInvoiceDocs, + ) +} + +func GenerateSnReply(sats int, nComments int) string { + return fmt.Sprintf("Notified OP on HN that their post received %d sats and %d comments.", sats, nComments) +} + func main() { stories := FetchHackerNewsTopStories() filtered := CurateContentForStackerNews(&stories) for _, story := range *filtered { PostStoryToStackerNews(&story) } + + items := FetchStackerNewsUserItems(SnUserName) + now := time.Now() + for _, item := range *items { + duration := now.Sub(item.CreatedAt) + if duration >= 24*time.Hour && item.Sats > 0 { + log.Printf("Found SN item (id=%d) older than 24 hours with %d sats and %d comments\n", item.Id, item.Sats, item.NComments) + for _, comment := range item.Comments { + if comment.User.Name == SnUserName { + snReply := GenerateSnReply(item.Sats, item.NComments) + // Check if OP on HN was already notified + alreadyNotified := false + for _, comment2 := range comment.Comments { + if comment2.User.Name == SnUserName { + alreadyNotified = true + } + } + if alreadyNotified { + log.Println("OP on HN was already notified") + break + } + text := comment.Text + hnItemId := FindHackerNewsItemId(text) + hnComment := GenerateHnComment(item.Id, item.Sats, item.NComments) + CommentHackerNewsStory(hnComment, hnItemId) + CommentStackerNewsPost(snReply, comment.Id) + break + } + } + } + } } diff --git a/sn.go b/sn.go index 8c20e34..1765e0a 100644 --- a/sn.go +++ b/sn.go @@ -34,17 +34,19 @@ type User struct { Name string `json:"name"` } type Comment struct { - Id int `json:"id,string"` - Text string `json:"text"` - User User `json:"user"` + Id int `json:"id,string"` + Text string `json:"text"` + User User `json:"user"` + Comments []Comment `json:"comments"` } type Item struct { Id int `json:"id,string"` Title string `json:"title"` Url string `json:"url"` Sats int `json:"sats"` - CreatedAt string `json:"createdAt"` + CreatedAt time.Time `json:"createdAt"` Comments []Comment `json:"comments"` + NComments int `json:"ncomments"` } type UpsertLinkResponse struct { @@ -63,11 +65,13 @@ type ItemsResponse struct { } var ( - SnApiUrl string - SnAuthCookie string + StackerNewsUrl string + SnApiUrl string + SnAuthCookie string ) func init() { + StackerNewsUrl = "https://stacker.news" SnApiUrl = "https://stacker.news/api/graphql" err := godotenv.Load() if err != nil { @@ -207,12 +211,22 @@ func FetchStackerNewsUserItems(user string) *[]Item { title url sats + createdAt comments { + id text user { name } + comments { + id + text + user { + name + } + } } + ncomments } cursor } @@ -251,3 +265,7 @@ func FetchStackerNewsUserItems(user string) *[]Item { return &items } + +func StackerNewsItemLink(id int) string { + return fmt.Sprintf("%s/items/%d", StackerNewsUrl, id) +}