refactor: split into files

This commit is contained in:
httpjamesm 2022-12-27 23:53:28 -05:00
parent e0a4b2363f
commit 076f1387f0
4 changed files with 224 additions and 177 deletions

184
main.go
View File

@ -1,200 +1,30 @@
package main
import (
"fmt"
"html/template"
"strings"
"anonymousoverflow/src/middleware"
"anonymousoverflow/src/routes"
"github.com/PuerkitoBio/goquery"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)
func main() {
client := resty.New()
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.Static("/static", "./public")
r.Use(middleware.OptionsMiddleware())
r.GET("/robots.txt", func(c *gin.Context) {
c.String(200, "User-agent: *\nDisallow: /")
})
r.GET("/", func(c *gin.Context) {
c.HTML(200, "home.html", gin.H{})
})
r.GET("/", routes.GetHome)
type urlConversionRequest struct {
URL string `form:"url" binding:"required"`
}
r.POST("/", routes.PostHome)
r.POST("/", func(c *gin.Context) {
body := urlConversionRequest{}
r.GET("/questions/:id/:title", routes.ViewQuestion)
if err := c.ShouldBind(&body); err != nil {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid request body",
})
return
}
soLink := body.URL
// validate URL
if !strings.HasPrefix(soLink, "https://stackoverflow.com/questions/") {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid stack overflow URL",
})
return
}
// redirect to the proxied thread
c.Redirect(302, fmt.Sprintf("/questions/%s", strings.TrimPrefix(soLink, "https://stackoverflow.com/questions/")))
})
r.GET("/questions/:id/:title", func(c *gin.Context) {
questionId := c.Param("id")
questionTitle := c.Param("title")
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s", questionId, questionTitle)
resp, err := client.R().Get(soLink)
if err != nil {
panic(err)
}
respBody := resp.String()
respBodyReader := strings.NewReader(respBody)
doc, err := goquery.NewDocumentFromReader(respBodyReader)
if err != nil {
panic(err)
}
questionTextParent := doc.Find("h1.fs-headline1")
questionText := questionTextParent.Children().First().Text()
questionBodyParent := doc.Find("div.s-prose")
questionBodyParentHTML, err := questionBodyParent.Html()
if err != nil {
panic(err)
}
questionCard := doc.Find("div.postcell")
questionMetadata := questionCard.Find("div.user-info")
questionTimestamp := ""
questionMetadata.Find("span.relativetime").Each(func(i int, s *goquery.Selection) {
// get the second
if i == 0 {
if s.Text() != "" {
// if it's not been edited, it means it's the first
questionTimestamp = s.Text()
return
}
}
// otherwise it's the second element
if i == 1 {
questionTimestamp = s.Text()
return
}
})
userDetails := questionMetadata.Find("div.user-details")
questionAuthor := ""
questionAuthorURL := ""
userDetails.Find("a").Each(func(i int, s *goquery.Selection) {
// get the second
if i == 0 {
if s.Text() != "" {
// if it's not been edited, it means it's the first
questionAuthor = s.Text()
questionAuthorURL, _ = s.Attr("href")
return
}
}
// otherwise it's the second element
if i == 1 {
questionAuthor = s.Text()
questionAuthorURL, _ = s.Attr("href")
return
}
})
answers := []template.HTML{}
doc.Find("div.answer").Each(func(i int, s *goquery.Selection) {
postLayout := s.Find("div.post-layout")
voteCell := postLayout.Find("div.votecell")
answerCell := postLayout.Find("div.answercell")
answerBody := answerCell.Find("div.s-prose")
answerBodyHTML, _ := answerBody.Html()
voteCount := voteCell.Find("div.js-vote-count").Text()
if s.HasClass("accepted-answer") {
// add <div class="answer-meta accepted">Accepted Answer</div> to the top of the answer
answerBodyHTML = fmt.Sprintf(`<div class="answer-meta accepted">Accepted Answer - %s Upvotes</div>`, voteCount) + answerBodyHTML
} else {
// add <div class="answer-meta">%s Upvotes</div> to the top of the answer
answerBodyHTML = fmt.Sprintf(`<div class="answer-meta">%s Upvotes</div>`, voteCount) + answerBodyHTML
}
answerFooter := s.Find("div.mt24")
answerAuthorURL := ""
answerAuthorName := ""
answerTimestamp := ""
answerFooter.Find("div.post-signature").Each(func(i int, s *goquery.Selection) {
answerAuthorDetails := s.Find("div.user-details")
if answerAuthorDetails.Length() == 0 {
return
}
if answerAuthorDetails.Length() > 1 {
if i == 0 {
return
}
}
answerAuthor := answerAuthorDetails.Find("a").First()
answerAuthorURL = answerAuthor.AttrOr("href", "")
answerAuthorName = answerAuthor.Text()
answerTimestamp = s.Find("span.relativetime").Text()
})
// append <div class="answer-author">Answered %s by %s</div> to the bottom of the answer
answerBodyHTML += fmt.Sprintf(`<div class="answer-author-parent"><div class="answer-author">Answered at %s by <a href="https://stackoverflow.com/%s" target="_blank" rel="noopener noreferrer">%s</a></div></div>`, answerTimestamp, answerAuthorURL, answerAuthorName)
// get the timestamp and author
answers = append(answers, template.HTML(answerBodyHTML))
})
c.HTML(200, "question.html", gin.H{
"title": questionText,
"body": template.HTML(questionBodyParentHTML),
"timestamp": questionTimestamp,
"author": questionAuthor,
"authorURL": questionAuthorURL,
"answers": answers,
})
})
r.Run("localhost:8080")
}

23
src/middleware/options.go Normal file
View File

@ -0,0 +1,23 @@
package middleware
import "github.com/gin-gonic/gin"
func OptionsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("disable_images", false)
// get cookie
cookie, err := c.Cookie("disable_images")
if err != nil {
c.Next()
return
}
// check if disable_images is equal to "true"
if cookie == "true" {
c.Set("disable_images", true)
}
c.Next()
}
}

41
src/routes/home.go Normal file
View File

@ -0,0 +1,41 @@
package routes
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
func GetHome(c *gin.Context) {
c.HTML(200, "home.html", gin.H{})
}
type urlConversionRequest struct {
URL string `form:"url" binding:"required"`
}
func PostHome(c *gin.Context) {
body := urlConversionRequest{}
if err := c.ShouldBind(&body); err != nil {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid request body",
})
return
}
soLink := body.URL
// validate URL
if !strings.HasPrefix(soLink, "https://stackoverflow.com/questions/") {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid stack overflow URL",
})
return
}
// redirect to the proxied thread
c.Redirect(302, fmt.Sprintf("/questions/%s", strings.TrimPrefix(soLink, "https://stackoverflow.com/questions/")))
}

153
src/routes/question.go Normal file
View File

@ -0,0 +1,153 @@
package routes
import (
"fmt"
"html/template"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)
func ViewQuestion(c *gin.Context) {
client := resty.New()
questionId := c.Param("id")
questionTitle := c.Param("title")
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s", questionId, questionTitle)
resp, err := client.R().Get(soLink)
if err != nil {
panic(err)
}
respBody := resp.String()
respBodyReader := strings.NewReader(respBody)
doc, err := goquery.NewDocumentFromReader(respBodyReader)
if err != nil {
panic(err)
}
questionTextParent := doc.Find("h1.fs-headline1")
questionText := questionTextParent.Children().First().Text()
questionBodyParent := doc.Find("div.s-prose")
questionBodyParentHTML, err := questionBodyParent.Html()
if err != nil {
panic(err)
}
questionCard := doc.Find("div.postcell")
questionMetadata := questionCard.Find("div.user-info")
questionTimestamp := ""
questionMetadata.Find("span.relativetime").Each(func(i int, s *goquery.Selection) {
// get the second
if i == 0 {
if s.Text() != "" {
// if it's not been edited, it means it's the first
questionTimestamp = s.Text()
return
}
}
// otherwise it's the second element
if i == 1 {
questionTimestamp = s.Text()
return
}
})
userDetails := questionMetadata.Find("div.user-details")
questionAuthor := ""
questionAuthorURL := ""
userDetails.Find("a").Each(func(i int, s *goquery.Selection) {
// get the second
if i == 0 {
if s.Text() != "" {
// if it's not been edited, it means it's the first
questionAuthor = s.Text()
questionAuthorURL, _ = s.Attr("href")
return
}
}
// otherwise it's the second element
if i == 1 {
questionAuthor = s.Text()
questionAuthorURL, _ = s.Attr("href")
return
}
})
answers := []template.HTML{}
doc.Find("div.answer").Each(func(i int, s *goquery.Selection) {
postLayout := s.Find("div.post-layout")
voteCell := postLayout.Find("div.votecell")
answerCell := postLayout.Find("div.answercell")
answerBody := answerCell.Find("div.s-prose")
answerBodyHTML, _ := answerBody.Html()
voteCount := voteCell.Find("div.js-vote-count").Text()
if s.HasClass("accepted-answer") {
// add <div class="answer-meta accepted">Accepted Answer</div> to the top of the answer
answerBodyHTML = fmt.Sprintf(`<div class="answer-meta accepted">Accepted Answer - %s Upvotes</div>`, voteCount) + answerBodyHTML
} else {
// add <div class="answer-meta">%s Upvotes</div> to the top of the answer
answerBodyHTML = fmt.Sprintf(`<div class="answer-meta">%s Upvotes</div>`, voteCount) + answerBodyHTML
}
answerFooter := s.Find("div.mt24")
answerAuthorURL := ""
answerAuthorName := ""
answerTimestamp := ""
answerFooter.Find("div.post-signature").Each(func(i int, s *goquery.Selection) {
answerAuthorDetails := s.Find("div.user-details")
if answerAuthorDetails.Length() == 0 {
return
}
if answerAuthorDetails.Length() > 1 {
if i == 0 {
return
}
}
answerAuthor := answerAuthorDetails.Find("a").First()
answerAuthorURL = answerAuthor.AttrOr("href", "")
answerAuthorName = answerAuthor.Text()
answerTimestamp = s.Find("span.relativetime").Text()
})
// append <div class="answer-author">Answered %s by %s</div> to the bottom of the answer
answerBodyHTML += fmt.Sprintf(`<div class="answer-author-parent"><div class="answer-author">Answered at %s by <a href="https://stackoverflow.com/%s" target="_blank" rel="noopener noreferrer">%s</a></div></div>`, answerTimestamp, answerAuthorURL, answerAuthorName)
// get the timestamp and author
answers = append(answers, template.HTML(answerBodyHTML))
})
c.HTML(200, "question.html", gin.H{
"title": questionText,
"body": template.HTML(questionBodyParentHTML),
"timestamp": questionTimestamp,
"author": questionAuthor,
"authorURL": questionAuthorURL,
"answers": answers,
})
}