commit dc5d5b0653e8da853891d1feeab43874bd0bd07b Author: Christine Dodrill Date: Sat Feb 27 15:44:57 2021 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90565d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +var/* \ No newline at end of file diff --git a/discordwebhook.go b/discordwebhook.go new file mode 100644 index 0000000..8f55c75 --- /dev/null +++ b/discordwebhook.go @@ -0,0 +1,60 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" +) + +// Webhook is the parent structure fired off to Discord. +type Webhook struct { + Embeds []Embed `json:"embeds,omitifempty"` +} + +// EmbedField is an individual field being embedded in a message. +type EmbedField struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline"` +} + +// EmbedFooter is the message footer. +type EmbedFooter struct { + Text string `json:"text"` + IconURL string `json:"icon_url"` +} + +// Embed is a set of embedded fields and a footer. +type Embed struct { + Title string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + Fields []EmbedField `json:"fields"` + Footer EmbedFooter `json:"footer"` +} + +// Send returns a request for a Discord webhook. +func Send(whurl string, w Webhook) *http.Request { + data, err := json.Marshal(&w) + if err != nil { + panic(err) + } + + req, err := http.NewRequest(http.MethodPost, whurl, bytes.NewBuffer(data)) + if err != nil { + panic(err) + } + + req.Header.Set("Content-Type", "application/json") + + return req +} + +// Validate validates the response from Discord. +func Validate(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return NewError(http.StatusOK, resp) + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..452d80a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module tulpa.dev/cadey/snoo2nebby + +go 1.16 + +require github.com/turnage/graw v0.0.0-20201204201853-a177df1b5c91 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ae316f3 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/turnage/graw v0.0.0-20201204201853-a177df1b5c91 h1:vYoyWnsUWuvaLGe6369mItyePB2EVFRjrvkev7xFuGQ= +github.com/turnage/graw v0.0.0-20201204201853-a177df1b5c91/go.mod h1:aAkq4I/q1izZSSwHvzhDn9NA+eGxgTSuibwP3MZRlQY= +github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb h1:qR56NGRvs2hTUbkn6QF8bEJzxPIoMw3Np3UigBeJO5A= +github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb/go.mod h1:GyqJdEoZSNoxKDb7Z2Lu/bX63jtFukwpaTP9ZIS5Ei0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4e10a34 --- /dev/null +++ b/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/turnage/graw/reddit" + "github.com/turnage/graw/streams" +) + +// UA is the user agent for Reddit +const UA = `NixOS:tulpa.dev/cadey/snoo2nebby:v0.1.0 (by /u/shadowh511)` + +var ( + webhookFile = flag.String("webhook-file", "./var/webhook.txt", "where the Discord webhook file is located") + subreddit = flag.String("subreddit", "tulpas", "the subreddit to monitor") + pokeFreq = flag.Duration("poke-frequency", 5*time.Minute, "how often the bot should poke the feed") +) + +func main() { + flag.Parse() + + script, err := reddit.NewScript(UA, *pokeFreq) + if err != nil { + log.Fatal(err) + } + + whSlc, err := os.ReadFile(*webhookFile) + if err != nil { + log.Fatal(err) + } + whURL := string(bytes.TrimSpace(whSlc)) + + kill := make(chan bool) + errs := make(chan error) + + stream, err := streams.Subreddits(script, kill, errs, *subreddit) + if err != nil { + log.Fatal(err) + } + + go func(errs chan error) { + for err := range errs { + log.Printf("%v", err) + } + }(errs) + + log.Printf("listening for new posts on /r/%s", *subreddit) + + for post := range stream { + log.Printf("got new post: by /u/%s: %q %s", post.Author, post.URL, post.Title) + wh := Webhook{ + Embeds: []Embed{ + { + Title: post.Title, + URL: post.URL, + Description: post.SelfText, + Footer: EmbedFooter{ + Text: "by /u/" + post.Author, + }, + }, + }, + } + + req := Send(whURL, wh) + req.Header.Set("User-Agent", UA) + resp, err := http.DefaultClient.Do(req) + if err != nil { + errs <- fmt.Errorf("can't send webhook: %w", err) + continue + } + err = Validate(resp) + if err != nil { + errs <- fmt.Errorf("can't validate response: %w", err) + continue + } + } +} diff --git a/var/.gitkeep b/var/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web.go b/web.go new file mode 100644 index 0000000..3a7396d --- /dev/null +++ b/web.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +// NewError creates an Error based on an expected HTTP status code vs data populated +// from an HTTP response. +// +// This consumes the body of the HTTP response. +func NewError(wantStatusCode int, resp *http.Response) error { + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + resp.Body.Close() + + loc, err := resp.Location() + if err != nil { + loc = resp.Request.URL + } + + return &Error{ + WantStatus: wantStatusCode, + GotStatus: resp.StatusCode, + URL: loc, + Method: resp.Request.Method, + ResponseBody: string(data), + } +} + +// Error is a web response error. Use this when API calls don't work out like you wanted them to. +type Error struct { + WantStatus, GotStatus int + URL *url.URL + Method string + ResponseBody string +} + +func (e Error) Error() string { + return fmt.Sprintf("%s %s: wanted status code %d, got: %d: %v", e.Method, e.URL, e.WantStatus, e.GotStatus, e.ResponseBody) +}