From d4fd93546c7cff8a29bc987851edaeed2c5cc7b2 Mon Sep 17 00:00:00 2001 From: mgerb42 Date: Thu, 23 Feb 2017 06:03:29 +0000 Subject: [PATCH] init --- .gitattributes | 1 + .gitignore | 3 + README.md | 9 ++ main.go | 368 +++++++++++++++++++++++++++++++++++++++++++++++++ nsfw.jpg | Bin 0 -> 10190 bytes 5 files changed, 381 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 main.go create mode 100644 nsfw.jpg diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1003d3bd --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* !text !filter !merge !diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4c67f33a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +reddit.db +top-of-reddit + diff --git a/README.md b/README.md new file mode 100644 index 00000000..33407425 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Top of Reddit + +- Every post that makes it to the front page of [r/all](http://reddit.com/r/all). +- Updated daily. +- Sorted by highest score achieved. +- GoLang +- [BoltDB](https://github.com/boltdb/bolt) for persistance. + +Inspired by [github-trending](https://github.com/josephyzhou/github-trending). diff --git a/main.go b/main.go new file mode 100644 index 00000000..e3409d12 --- /dev/null +++ b/main.go @@ -0,0 +1,368 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/boltdb/bolt" + "github.com/tidwall/gjson" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "sort" + "strconv" + "time" +) + +const ( + REDDIT_URL string = "https://www.reddit.com/r/" + USER_AGENT string = "top-of-reddit:bot" + DATE_FORMAT string = "01-02-2006" +) + +var ( + // buckets + DAILY_BUCKET []byte = []byte("daily_bucket") + MAIN_BUCKET []byte = []byte("main") // main bucket for keeping track of the current day + + // keys + TODAY_KEY []byte = []byte("today_date") +) + +type RedditPost struct { + Subreddit string `json:"subreddit"` + ID string `json:"id"` + Gilded int `json:"gilded"` + Score int `json:"score"` + Author string `json:"author"` + Domain string `json:"domain"` + Over_18 bool `json:"over_18"` + Thumbnail string `json:"thumbnail"` + Permalink string `json:"permalink"` + Url string `json:"url"` + Title string `json:"title"` + Created float64 `json:"created"` + Created_utc float64 `json:"created_utc"` + Num_comments int `json:"num_comments"` + Ups int `json:"ups"` + + // extra fields + TopPosition int // highest achieved position on front page +} + +func main() { + // start database connection + db := openDbSession() + defer db.Close() + + // start main program loop + for { + fmt.Println("Updating...") + // send http request for json data + response, err := getPosts("all") + if err != nil { + log.Println(err.Error()) + } else { + // create RedditPost slice + posts, err := convertPosts(response) + if err != nil { + log.Println(err.Error()) + } else { + // update the daily bucket with posts + updateDailyPosts(db, DAILY_BUCKET, getTodayBucket(), posts) + checkDateChange(db) + } + + } + + time.Sleep(time.Second * 30) + } +} + +// start the main database session +func openDbSession() *bolt.DB { + database, err := bolt.Open("reddit.db", 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + log.Fatal(err) + } + + return database +} + +// returns the post bucket for today +func getTodayBucket() []byte { + return []byte(time.Now().Format(DATE_FORMAT)) +} + +// returns the post bucket for today +func getYesterdayBucket() []byte { + yesterday := time.Now().AddDate(0, 0, -1) + return []byte(yesterday.Format(DATE_FORMAT)) +} + +func checkDateChange(db *bolt.DB) { + err := db.Update(func(tx *bolt.Tx) error { + + b, err := tx.CreateBucketIfNotExists(MAIN_BUCKET) + + if err != nil { + return err + } + + storedDay := b.Get(TODAY_KEY) + + // if the day changes + if storedDay == nil || string(getTodayBucket()) != string(storedDay) { + // set today's date in database + err := b.Put(TODAY_KEY, []byte(getTodayBucket())) + + if err != nil { + return err + } + + if storedDay == nil { + storedDay = getTodayBucket() + } + + // if there was a previous stored key todayDate - create markdown file + fmt.Println("Creating markdown!") + + storedPosts, err := getStoredPosts(db, DAILY_BUCKET, storedDay) + + if err != nil { + return err + } + + err = writePostsToFile(string(storedDay), storedPosts) + + if err != nil { + return err + } + + // push to github + err = pushToGithub() + + if err != nil { + return err + } + } + + return nil + }) + + if err != nil { + log.Println(err) + return + } +} + +func writePostsToFile(fileName string, posts []RedditPost) error { + // create new markdown file + file, err := os.Create(fileName + ".md") + defer file.Close() + + if err != nil { + return err + } + + for index, p := range posts { + permalink := "http://reddit.com" + p.Permalink + file.WriteString("## " + strconv.Itoa(index+1) + ". [" + p.Title + "](" + permalink + ") - " + strconv.Itoa(p.Score) + "\n") + file.WriteString("#### [r/" + p.Subreddit + "](http://reddit.com/r/" + p.Subreddit + ")") + file.WriteString(" - [u/" + p.Author + "](http://reddit.com/u/" + p.Author + ") - ") + file.WriteString(strconv.Itoa(p.Num_comments) + " Comments - ") + file.WriteString("Top position achieved: " + strconv.Itoa(p.TopPosition) + "\n\n") + + // don't show thumbnail if NSFW + if p.Over_18 { + file.WriteString("\n\n") + } else { + file.WriteString("\n\n") + } + } + + file.Sync() + + return nil +} + +// get a RedditPost slice +func getStoredPosts(db *bolt.DB, bucket []byte, day []byte) ([]RedditPost, error) { + + posts := []RedditPost{} + + err := db.View(func(tx *bolt.Tx) error { + tx.Bucket(bucket).Bucket(day).ForEach(func(_, v []byte) error { + tempPost := RedditPost{} + err := json.Unmarshal(v, &tempPost) + posts = append(posts, tempPost) + sort.Sort(ByScore(posts)) + + if err != nil { + return err + } + + return nil + }) + + return nil + }) + + if err != nil { + return []RedditPost{}, err + } + + return posts, nil +} + +// stores new posts in the bucket only if they do not exist +func updateDailyPosts(db *bolt.DB, bucket []byte, day []byte, redditPosts []RedditPost) error { + err := db.Update(func(tx *bolt.Tx) error { + + daily_bucket, err := tx.CreateBucketIfNotExists(bucket) + if err != nil { + return err + } + + today, err := daily_bucket.CreateBucketIfNotExists(day) + if err != nil { + return err + } + + for index, post := range redditPosts { + // check if post was in yesterdays top posts + yesterday := daily_bucket.Bucket(getYesterdayBucket()) + if yesterday != nil && yesterday.Get([]byte(post.ID)) != nil { + continue + } + + post.TopPosition = index + 1 + + // get value stored in database + storedPostString := today.Get([]byte(post.ID)) + + // if post is already stored in database - check to update highest score + if storedPostString != nil { + storedPost := RedditPost{} + err := json.Unmarshal(storedPostString, &storedPost) + if err != nil { + return err + } + + // only store the highest score a post receives + if storedPost.Score > post.Score { + post.Score = storedPost.Score + } + + // only store the highest position a post receives + if storedPost.TopPosition > index+1 { + post.TopPosition = storedPost.TopPosition + } + } else { + fmt.Println("Updating new post: " + post.Title) + } + + // serialize json + postString, err := json.Marshal(post) + if err != nil { + return err + } + + // store in database + err = today.Put([]byte(post.ID), []byte(postString)) + if err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} + +// convert reddit response string to RedditPost slice +func convertPosts(postString string) ([]RedditPost, error) { + posts := []RedditPost{} + + for _, p := range gjson.Get(postString, "data.children").Array() { + tempPost := RedditPost{} + + err := json.Unmarshal([]byte(p.Get("data").String()), &tempPost) + if err != nil { + return posts, err + } + + posts = append(posts, tempPost) + } + + return posts, nil +} + +// send http request to reddit and obtain the response string +func getPosts(subreddit string) (string, error) { + client := &http.Client{} + + req, err := http.NewRequest("GET", REDDIT_URL+subreddit+".json", nil) + + req.Header.Add("User-Agent", USER_AGENT) + + response, err := client.Do(req) + if err != nil { + return "", err + } + + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + + return string(body), nil +} + +func pushToGithub() error { + fmt.Println("Pushing to Github...") + commitMessage := "Adding posts for " + string(getTodayBucket()) + + out, err := exec.Command("git", "add", ".").Output() + if err != nil { + return err + } + fmt.Println(string(out)) + + out, err = exec.Command("git", "commit", "-m", commitMessage).Output() + if err != nil { + return err + } + fmt.Println(string(out)) + + out, err = exec.Command("git", "push", "origin", "master").Output() + if err != nil { + return err + } + fmt.Println(string(out)) + + return nil +} + +// sorting +type ByScore []RedditPost + +func (s ByScore) Len() int { + return len(s) +} + +func (s ByScore) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ByScore) Less(i, j int) bool { + return s[i].Score > s[j].Score +} diff --git a/nsfw.jpg b/nsfw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5030c4db014996bf4e319225b7b3e1ab0fd734d9 GIT binary patch literal 10190 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 VK*mnZ|5|?k3Mcme4)%BMe*iqjUrPW0 literal 0 HcmV?d00001