// HandleHome displays a list of stories using gravity to order them // used for the home page for gravity rank see votes.go // responds to GET / func HandleHome(context router.Context) error { // Build a query q := stories.Query().Limit(listLimit) // Select only above 0 points, Order by rank, then points, then name q.Where("points > 0").Order("rank desc, points desc, id desc") // Set the offset in pages if we have one page := int(context.ParamInt("page")) if page > 0 { q.Offset(listLimit * page) } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) setStoriesMetadata(view, context.Request()) view.AddKey("page", page) view.AddKey("stories", results) view.Template("stories/views/index.html.got") if context.Param("format") == ".xml" { view.Layout("") view.Template("stories/views/index.xml.got") } return view.Render() }
// TweetTopStory tweets the top story func TweetTopStory(context schedule.Context) { context.Log("Sending top story tweet") // Get the top story which has not been tweeted yet, newer than 1 day (we don't look at older stories) q := stories.Popular().Limit(1).Order("rank desc, points desc, id desc") // Don't fetch old stories - at some point soon this can come down to 1 day // as all older stories will have been tweeted // For now omit this as we have a backlog of old unposted stories // q.Where("created_at > current_timestamp - interval '60 days'") // Don't fetch stories that have already been tweeted q.Where("tweeted_at IS NULL") // Fetch the stories results, err := stories.FindAll(q) if err != nil { context.Logf("#error getting top story tweet %s", err) return } if len(results) > 0 { story := results[0] TweetStory(context, story) } else { context.Logf("#warn no top story found for tweet") } }
// HandleHome displays a list of stories using gravity to order them // used for the home page for gravity rank see votes.go func HandleHome(context router.Context) error { // Build a query q := stories.Query().Limit(listLimit) // Select only above 0 points, Order by rank, then points, then name q.Where("points > 0").Order("rank desc, points desc, id desc") // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) view.AddKey("stories", results) view.AddKey("meta_title", "Golang News") view.AddKey("meta_desc", "News for Go Hackers, in the style of Hacker News. A curated selection of the latest links about the Go programming language.") view.AddKey("meta_keywords", "golang news, blog, links, go developers, go web apps, web applications, fragmenta") view.AddKey("authenticity_token", authorise.CreateAuthenticityToken(context)) view.Template("stories/views/index.html.got") return view.Render() }
// DailyEmail sends a daily email to subscribed users with top stories - change this to WeeklyEmail // before putting it into production // We should probably only do this for kenny at present func DailyEmail(context schedule.Context) { context.Log("Sending daily email") // First fetch our stories over 5 points q := stories.Popular() // Must be within 7 days q.Where("created_at > current_timestamp - interval '7 day'") // Order by rank q.Order("rank desc, points desc, id desc") // Don't fetch stories that have already been mailed q.Where("newsletter_at IS NULL") // Fetch the stories topStories, err := stories.FindAll(q) if err != nil { context.Logf("#error getting top story tweet %s", err) return } if len(topStories) == 0 { context.Logf("#warn no stories found for newsletter") return } // Now fetch our recipient (initially just Kenny as this is in testing) recipient, err := users.Find(1) if err != nil { context.Logf("#error getting email reciipents %s", err) return } var jobStories []*stories.Story // Email recipients the stories in question - we should perhaps save in db so that we can // have an issue number and always reproduce the digests? mailContext := map[string]interface{}{ "stories": topStories, "jobs": jobStories, } err = mail.SendOne(recipient.Email, "Go News Digest", "users/views/mail/digest.html.got", mailContext) if err != nil { context.Logf("#error sending email %s", err) return } // Record that these stories have been mailed in db params := map[string]string{"newsletter_at": query.TimeString(time.Now().UTC())} err = q.Order("").UpdateAll(params) if err != nil { context.Logf("#error updating top story newsletter_at %s", err) return } }
// TweetTopStory tweets the top story func TweetTopStory(context schedule.Context) { context.Log("Sending top story tweet") // Get the top story which has not been tweeted yet, newer than 1 day (we don't look at older stories) q := stories.Popular().Limit(1).Order("rank desc, points desc, id desc") // Don't fetch old stories q.Where("created_at > current_timestamp - interval '1 day'") // Don't fetch stories that have already been tweeted q.Where("tweeted_at IS NULL") // Fetch the stories results, err := stories.FindAll(q) if err != nil { context.Logf("#error getting top story tweet %s", err) return } if len(results) > 0 { story := results[0] // TWEET tweet := fmt.Sprintf("%s #golang %s", story.Name, story.Url) _, err := twitter.Tweet(tweet) if err != nil { context.Logf("#error tweeting top story %s", err) return } // Record that this story has been tweeted in db params := map[string]string{"tweeted_at": query.TimeString(time.Now().UTC())} err = story.Update(params) if err != nil { context.Logf("#error updating top story tweet %s", err) return } } else { context.Logf("#warn no top story found for tweet") } }
// HandleCode displays a list of stories linking to repos (github etc) using gravity to order them // responds to GET /stories/code func HandleCode(context router.Context) error { // Build a query q := stories.Query().Where("points > -6").Order("rank desc, points desc, id desc").Limit(listLimit) // Restrict to stories with have a url starting with github.com or bitbucket.org // other code repos can be added later q.Where("url ILIKE 'https://github.com%'").OrWhere("url ILIKE 'https://bitbucket.org'") // Set the offset in pages if we have one page := int(context.ParamInt("page")) if page > 0 { q.Offset(listLimit * page) } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) setStoriesMetadata(view, context.Request()) view.AddKey("page", page) view.AddKey("stories", results) // view.AddKey("tags", []string{"web", "mobile", "data", "email", "crypto", "data", "graphics", "ui", "security"}) // TODO: remove these calls and put in a filter // - given it is not too expensive, we could just generate tokens on every request view.Template("stories/views/index.html.got") if context.Param("format") == ".xml" { view.Layout("") view.Template("stories/views/index.xml.got") } return view.Render() }
// HandleCode displays a list of stories linking to repos (github etc) using gravity to order them // responds to GET /stories/code func HandleCode(context router.Context) error { // Build a query q := stories.Query().Where("points > -6").Order("rank desc, points desc, id desc").Limit(listLimit) // Restrict to stories with have a url starting with github.com or bitbucket.org // other code repos can be added later q.Where("url ILIKE 'https://github.com%'").OrWhere("url ILIKE 'https://bitbucket.org'") // Set the offset in pages if we have one page := int(context.ParamInt("page")) if page > 0 { q.Offset(listLimit * page) } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) view.AddKey("page", page) view.AddKey("stories", results) view.AddKey("pubdate", storiesModTime(results)) view.AddKey("meta_title", "Go Code") view.AddKey("meta_desc", context.Config("meta_desc")) view.AddKey("meta_keywords", context.Config("meta_keywords")) view.AddKey("meta_rss", storiesXMLPath(context)) view.Template("stories/views/index.html.got") if context.Param("format") == ".xml" { view.Layout("") view.Template("stories/views/index.xml.got") } return view.Render() }
// HandleIndex displays a list of stories at /stories func HandleIndex(context router.Context) error { // Build a query q := stories.Query().Limit(listLimit) // Order by date by default q.Where("points > -6").Order("created_at desc") // Filter if necessary - this assumes name and summary cols filter := context.Param("filter") if len(filter) > 0 { context.Logf("FILTER %s", filter) // Replace special characters with escaped sequence filter = strings.Replace(filter, "_", "\\_", -1) filter = strings.Replace(filter, "%", "\\%", -1) // initially very simple, do ilike query for filter with wildcards q.Where("stories.name ILIKE ?", "%"+filter+"%") // If filtering, order by rank, not by date q.Order("rank desc, points desc, id desc") } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) view.AddKey("filter", filter) view.AddKey("stories", results) view.AddKey("meta_title", "Go Hacker News Links") view.AddKey("authenticity_token", authorise.CreateAuthenticityToken(context)) return view.Render() }
// HandleHome displays a list of stories using gravity to order them // used for the home page for gravity rank see votes.go // responds to GET / func HandleHome(context router.Context) error { // Build a query q := stories.Query().Limit(listLimit) // Select only above 0 points, Order by rank, then points, then name q.Where("points > 0").Order("rank desc, points desc, id desc") // Set the offset in pages if we have one page := int(context.ParamInt("page")) if page > 0 { q.Offset(listLimit * page) } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) view.AddKey("page", page) view.AddKey("stories", results) view.Template("stories/views/index.html.got") view.AddKey("pubdate", storiesModTime(results)) view.AddKey("meta_title", fmt.Sprintf("%s, %s", context.Config("meta_title"), context.Config("meta_desc"))) view.AddKey("meta_desc", context.Config("meta_desc")) view.AddKey("meta_keywords", context.Config("meta_keywords")) view.AddKey("meta_rss", storiesXMLPath(context)) if context.Param("format") == ".xml" { view.Layout("") view.Template("stories/views/index.xml.got") } return view.Render() }
// HandleShow serve a get request at /users/1 func HandleShow(context router.Context) error { // No auth - this is public // Find the user user, err := users.Find(context.ParamInt("id")) if err != nil { context.Logf("#error parsing user id: %s", err) return router.NotFoundError(err) } // Get the user comments q := comments.Where("user_id=?", user.Id).Limit(10).Order("created_at desc") userComments, err := comments.FindAll(q) if err != nil { return router.InternalError(err) } // Get the user stories q = stories.Where("user_id=?", user.Id).Limit(50).Order("created_at desc") userStories, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the Template view := view.New(context) view.AddKey("user", user) view.AddKey("comments", userComments) view.AddKey("stories", userStories) view.AddKey("meta_title", user.Name) view.AddKey("meta_desc", user.Name) return view.Render() }
// HandleIndex displays a list of stories at /stories func HandleIndex(context router.Context) error { // Build a query q := stories.Query().Limit(listLimit) // Order by date by default q.Where("points > -6").Order("created_at desc") // Filter if necessary - this assumes name and summary cols filter := context.Param("q") if len(filter) > 0 { // Replace special characters with escaped sequence filter = strings.Replace(filter, "_", "\\_", -1) filter = strings.Replace(filter, "%", "\\%", -1) wildcard := "%" + filter + "%" // Perform a wildcard search for name or url q.Where("stories.name ILIKE ? OR stories.url ILIKE ?", wildcard, wildcard) // If filtering, order by rank, not by date q.Order("rank desc, points desc, id desc") } // Set the offset in pages if we have one page := int(context.ParamInt("page")) if page > 0 { q.Offset(listLimit * page) } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } // Render the template view := view.New(context) /* // Consider how best to do this switch filter { case "Hiring:": view.AddKey("tags", []string{"sf", "nyc", "boston", "london", "berlin"}) default: view.AddKey("tags", []string{"web", "mobile", "graphics", "security"}) } */ setStoriesMetadata(view, context.Request()) view.AddKey("page", page) view.AddKey("stories", results) view.AddKey("meta_title", "Golang News links") if context.Param("format") == ".xml" { view.Layout("") view.Template("stories/views/index.xml.got") } return view.Render() }
// HandleIndex displays a list of stories at /stories func HandleIndex(context router.Context) error { // Build a query q := stories.Query().Limit(listLimit) // Order by date by default q.Where("points > -6").Order("created_at desc") // Filter if necessary - this assumes name and summary cols filter := context.Param("q") if len(filter) > 0 { // Replace special characters with escaped sequence filter = strings.Replace(filter, "_", "\\_", -1) filter = strings.Replace(filter, "%", "\\%", -1) wildcard := "%" + filter + "%" // Perform a wildcard search for name or url q.Where("stories.name ILIKE ? OR stories.url ILIKE ?", wildcard, wildcard) // If filtering, order by rank, not by date q.Order("rank desc, points desc, id desc") } // Set the offset in pages if we have one page := int(context.ParamInt("page")) if page > 0 { q.Offset(listLimit * page) } // Fetch the stories results, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } windowTitle := context.Config("meta_title") switch filter { case "Video:": windowTitle = "Golang Videos" } // Render the template view := view.New(context) view.AddKey("page", page) view.AddKey("stories", results) view.AddKey("pubdate", storiesModTime(results)) view.AddKey("meta_title", windowTitle) view.AddKey("meta_desc", context.Config("meta_desc")) view.AddKey("meta_keywords", context.Config("meta_keywords")) view.AddKey("meta_rss", storiesXMLPath(context)) if context.Param("format") == ".xml" { view.Layout("") view.Template("stories/views/index.xml.got") } return view.Render() }
// HandleCreate handles the POST of the create form for stories func HandleCreate(context router.Context) error { // Check csrf token err := authorise.AuthenticityToken(context) if err != nil { return router.NotAuthorizedError(err) } // Check permissions - if not logged in and above 1 points, redirect to error if !authorise.CurrentUser(context).CanSubmit() { return router.NotAuthorizedError(nil, "Sorry", "You need to be registered and have more than 1 points to submit stories.") } // Get user details user := authorise.CurrentUser(context) ip := getUserIP(context) // Check that no story with this url already exists q := stories.Where("url=?", context.Param("url")) duplicates, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } if len(duplicates) > 0 { dupe := duplicates[0] // Add a point to dupe addStoryVote(dupe, user, ip, 1) return router.Redirect(context, dupe.URLShow()) } // Setup context params, err := context.Params() if err != nil { return router.InternalError(err) } // Set a few params params.SetInt("points", 1) params.SetInt("user_id", user.Id) params.Set("user_name", user.Name) id, err := stories.Create(params.Map()) if err != nil { return err // Create returns a router.Error } // Log creation context.Logf("#info Created story id,%d", id) // Redirect to the new story story, err := stories.Find(id) if err != nil { return router.InternalError(err) } // We need to add a vote to the story here too by adding a join to the new id err = recordStoryVote(story, user, ip, +1) if err != nil { return err } // Re-rank stories err = updateStoriesRank() if err != nil { return err } return router.Redirect(context, story.URLIndex()) }
// HandleCreate handles the POST of the create form for stories func HandleCreate(context router.Context) error { // Check csrf token err := authorise.AuthenticityToken(context) if err != nil { return router.NotAuthorizedError(err) } // Check permissions - if not logged in and above 1 points, redirect to error if !authorise.CurrentUser(context).CanSubmit() { return router.NotAuthorizedError(nil, "Sorry", "You need to be registered and have more than 1 points to submit stories.") } // Get params params, err := context.Params() if err != nil { return router.InternalError(err) } // Get user details user := authorise.CurrentUser(context) ip := getUserIP(context) url := params.Get("url") // Strip trailing slashes on url before comparisons // we could possibly also strip url fragments if strings.HasSuffix(url, "/") { url = strings.Trim(url, "/") params.Set("url", url) } // Check that no story with this url already exists q := stories.Where("url=?", url) duplicates, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } if len(duplicates) > 0 { dupe := duplicates[0] // Add a point to dupe and return addStoryVote(dupe, user, ip, 1) return router.Redirect(context, dupe.URLShow()) } // Clean params according to role accepted := stories.AllowedParams() if authorise.CurrentUser(context).Admin() { accepted = stories.AllowedParamsAdmin() } cleanedParams := params.Clean(accepted) // Set a few params cleanedParams["points"] = "1" cleanedParams["user_id"] = fmt.Sprintf("%d", user.Id) cleanedParams["user_name"] = user.Name id, err := stories.Create(cleanedParams) if err != nil { return err // Create returns a router.Error } // Log creation context.Logf("#info Created story id,%d", id) // Redirect to the new story story, err := stories.Find(id) if err != nil { return router.InternalError(err) } // We need to add a vote to the story here too by adding a join to the new id err = recordStoryVote(story, user, ip, +1) if err != nil { return err } // Re-rank stories err = updateStoriesRank() if err != nil { return err } return router.Redirect(context, story.URLIndex()) }
// HandleCreate handles the POST of the create form for stories func HandleCreate(context router.Context) error { // Check csrf token err := authorise.AuthenticityToken(context) if err != nil { return router.NotAuthorizedError(err) } // Check permissions - if not logged in and above 1 points, redirect to error if !authorise.CurrentUser(context).CanSubmit() { return router.NotAuthorizedError(nil, "Sorry", "You need to be registered and have more than 1 points to submit stories.") } // Get params params, err := context.Params() if err != nil { return router.InternalError(err) } // Get user details user := authorise.CurrentUser(context) ip := getUserIP(context) // Process urls url := params.Get("url") // Strip trailing slashes on url before comparisons if strings.HasSuffix(url, "/") { url = strings.Trim(url, "/") } // Strip ?utm_source etc - remove all after ?utm_source if strings.Contains(url, "?utm_") { url = strings.Split(url, "?utm_")[0] } // Strip url fragments (For example trailing # on medium urls) if strings.Contains(url, "#") { url = strings.Split(url, "#")[0] } // Rewrite mobile youtube links if strings.HasPrefix(url, "https://m.youtube.com") { url = strings.Replace(url, "https://m.youtube.com", "https://www.youtube.com", 1) } params.Set("url", url) // Check that no story with this url already exists q := stories.Where("url=?", url) duplicates, err := stories.FindAll(q) if err != nil { return router.InternalError(err) } if len(duplicates) > 0 { story := duplicates[0] // Check we have no votes already from this user, if we do fail if storyHasUserVote(story, user) { return router.NotAuthorizedError(err, "Vote Failed", "Sorry you are not allowed to vote twice, nice try!") } // Add a point to dupe and return addStoryVote(story, user, ip, 1) return router.Redirect(context, story.URLShow()) } // Clean params according to role accepted := stories.AllowedParams() if authorise.CurrentUser(context).Admin() { accepted = stories.AllowedParamsAdmin() } cleanedParams := params.Clean(accepted) // Set a few params cleanedParams["points"] = "1" cleanedParams["user_id"] = fmt.Sprintf("%d", user.Id) cleanedParams["user_name"] = user.Name id, err := stories.Create(cleanedParams) if err != nil { return err // Create returns a router.Error } // Log creation context.Logf("#info Created story id,%d", id) // Redirect to the new story story, err := stories.Find(id) if err != nil { return router.InternalError(err) } // We need to add a vote to the story here too by adding a join to the new id err = recordStoryVote(story, user, ip, +1) if err != nil { return err } // Re-rank stories err = updateStoriesRank() if err != nil { return err } return router.Redirect(context, story.URLIndex()) }