diff --git a/args.go b/args.go index 3c4bec9..a6cf053 100644 --- a/args.go +++ b/args.go @@ -12,6 +12,8 @@ type botOptions struct { LogConvIDStr string `env:"BOT_LOG_CONVID" envDefault:""` HomePath string `envDefault:""` JSON bool `env:"BOT_LOG_JSON" envDefault:"false"` + RedditUser string `env:"BOT_REDDIT_USER" envDefault:""` + RedditPass string `env:"BOT_REDDIT_PASS" envDefault:""` } func parseArgs(args []string) botOptions { @@ -27,6 +29,8 @@ func parseArgs(args []string) botOptions { flags.BoolVar(&cliOpts.JSON, "json", false, "enables JSON logging") flags.StringVar(&cliOpts.LogConvIDStr, "log-convid", "", "set the keybase conversation log id") flags.StringVar(&cliOpts.HomePath, "kbhome", "", "sets alternate keybase home folder for debugging") + flags.StringVar(&cliOpts.RedditUser, "reddit-user", "", "sets the reddit auth user") + flags.StringVar(&cliOpts.RedditPass, "reddit-pass", "", "sets the reddit auth password") if err := flags.Parse(args[1:]); err != nil { log.Fatalf("Unable to parse cli args: %+v", err) } @@ -44,6 +48,12 @@ func parseArgs(args []string) botOptions { if cliOpts.LogConvIDStr != "" { opts.LogConvIDStr = cliOpts.LogConvIDStr } + if cliOpts.RedditUser != "" { + opts.RedditUser = cliOpts.RedditUser + } + if cliOpts.RedditPass != "" { + opts.RedditPass = cliOpts.RedditPass + } } return opts } diff --git a/cmd/eyebleach.go b/cmd/eyebleach.go new file mode 100644 index 0000000..4b7bfe5 --- /dev/null +++ b/cmd/eyebleach.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "os" + + "github.com/kf5grd/keybasebot" + "github.com/teris-io/shortid" + "samhofi.us/x/keybase/v2/types/chat1" +) + +var EyebleachAd = chat1.UserBotCommandInput{ + Name: "eyebleach", + Usage: "", + Description: "Sends some cute critters", +} + +func Eyebleach(m chat1.MsgSummary, b *keybasebot.Bot) (bool, error) { + path, desc, err := getRandomRedditMedia(b, "eyebleach", "top", 20) + if err != nil { + eid := shortid.MustGenerate() + b.Logger.Error("%s: %+v", eid, err) + b.KB.ReactByConvID(m.ConvID, m.Id, "Error: %s", eid) + return true, nil + } + // upload the image + b.KB.UploadToConversation(m.ConvID, desc, path) + // delete the file + err = os.Remove(path) + if err != nil { + b.Logger.Error("Unable to remove file %s", path) + } + return true, nil +} diff --git a/cmd/reddit.go b/cmd/reddit.go new file mode 100644 index 0000000..b1941f6 --- /dev/null +++ b/cmd/reddit.go @@ -0,0 +1,162 @@ +package cmd + +import ( + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/google/uuid" + "github.com/jzelinskie/geddit" + "github.com/kf5grd/keybasebot" +) + +func getMetaLoginCreds(b *keybasebot.Bot) (username, password string, err error) { + user, ok := b.Meta["reddit-user"] + if !ok { + err = fmt.Errorf("No reddit username has been set.") + } + pass, ok := b.Meta["reddit-pass"] + if !ok { + err = fmt.Errorf("No reddit password has been set.") + } + username = user.(string) + password = pass.(string) + return +} + +func getRedditSubmissions(b *keybasebot.Bot, sub string, sort string, count int) ([]*geddit.Submission, error) { + // check the sort type + allowedSort := map[string]bool{ + "hot": true, + "new": true, + "rising": true, + "top": true, + "controversial": true, + "": true, + } + if !allowedSort[sort] { + return nil, fmt.Errorf("%s is not an allowed sort method", sort) + } + user, pass, err := getMetaLoginCreds(b) + if err != nil { + return nil, err + } + session, err := geddit.NewLoginSession( + user, + pass, + "golang:pub.keybase.haukened.ssh0le:v2.0 (by /u/no-names-here)", + ) + if err != nil { + return nil, err + } + subOpts := geddit.ListingOptions{ + Limit: count, + } + submissions, err := session.SubredditSubmissions(sub, geddit.PopularitySort(sort), subOpts) + if err != nil { + return nil, err + } + return submissions, nil +} + +func filterSubmissions(gs []*geddit.Submission, test func(*geddit.Submission) bool) (ret []*geddit.Submission) { + for _, s := range gs { + if test(s) { + ret = append(ret, s) + } + } + return +} + +func filterMedia(gs []*geddit.Submission) (ret []*geddit.Submission) { + test := func(s *geddit.Submission) bool { + return strings.HasSuffix(s.URL, ".jpg") || + strings.HasSuffix(s.URL, ".png") || + strings.HasSuffix(s.URL, ".gif") || + strings.HasSuffix(s.URL, ".mp4") + } + ret = filterSubmissions(gs, test) + return +} + +func fixGfycatURL(sub *geddit.Submission) error { + resp, err := http.Get(sub.URL) + if err != nil { + return err + } + defer resp.Body.Close() + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return err + } + vids := doc.Find("source") + found := false + for _, vid := range vids.Nodes { + for _, element := range vid.Attr { + if strings.HasSuffix(element.Val, ".mp4") && strings.Contains(element.Val, "-mobile") { + sub.URL = element.Val + found = true + break + } + } + } + if !found { + return fmt.Errorf("Unable to find Gfycat Video, because they suck.") + } + return nil +} + +func downloadRedditMedia(s *geddit.Submission) (path string, err error) { + // break down the url to get the path, less the host and any arguments or fragments + URLParts, err := url.Parse(s.URL) + if err != nil { + return + } + // get the file extension + ext := filepath.Ext(URLParts.Path) + if ext == "" { + err = fmt.Errorf("URL had no media file extension") + return + } + // get the file + response, err := http.Get(s.URL) + if err != nil { + return + } + defer response.Body.Close() + // open a tmp file for writing + uid := uuid.New() + path = fmt.Sprintf("/tmp/%s.%s", uid, ext) + file, err := os.Create(path) + if err != nil { + return + } + defer file.Close() + _, err = io.Copy(file, response.Body) + return +} + +func getRandomRedditMedia(b *keybasebot.Bot, sub string, sortby string, count int) (path, description string, err error) { + submissions, err := getRedditSubmissions(b, sub, sortby, count) + if err != nil { + return + } + mediaSubs := filterMedia(submissions) + // select a random post + rand.Seed(time.Now().Unix()) + randSub := mediaSubs[rand.Intn(len(mediaSubs))] + path, err = downloadRedditMedia(randSub) + if err != nil { + return + } + // generate the description + description = fmt.Sprintf("image by /u/%s", randSub.Author) + return +} diff --git a/main.go b/main.go index 0263d24..e79644c 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "os/signal" "syscall" @@ -20,6 +21,10 @@ func main() { b.JSON = opts.JSON b.LogWriter = os.Stdout b.LogConv = chat1.ConvIDStr(opts.LogConvIDStr) + if opts.RedditUser != "" && opts.RedditPass != "" { + b.Meta["reddit-user"] = opts.RedditUser + b.Meta["reddit-pass"] = opts.RedditPass + } // register the bot commands b.Commands = append(b.Commands, @@ -44,6 +49,18 @@ func main() { Run: keybasebot.Adapt(cmd.SendPrice, keybasebot.MessageType("text"), keybasebot.CommandPrefix("!price")), }, ) + // if there are reddit credentials add the reddit commands + if opts.RedditPass != "" && opts.RedditUser != "" { + b.Commands = append(b.Commands, + keybasebot.BotCommand{ + Name: "eyebleach", + Ad: &cmd.EyebleachAd, + Run: keybasebot.Adapt(cmd.Eyebleach, keybasebot.MessageType("text"), keybasebot.CommandPrefix("!eyebleach")), + }, + ) + fmt.Printf("%+v\n", opts) + } + // catch ctrl-c so we can clean up c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM)