mirror of
https://github.com/httpjamesm/AnonymousOverflow.git
synced 2024-12-06 19:16:43 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdca47a51e | |||
| 51190f8d69 | |||
| bfa19bc2d2 | |||
| 55b06b986b | |||
| 09de9f11a5 | |||
| 63e9792d2a | |||
| 8ee1d76993 | |||
| 6b77c5dae4 | |||
| df93607e5d | |||
| 05fbd9608e | |||
| 66db070fa5 | |||
| bb9f8e2aa2 | |||
| 487a62e7aa | |||
| abfd0cfb6f | |||
| 63a30ab1cf | |||
| f1370c29b6 | |||
| d935ce72f6 | |||
| 45644a0503 | |||
| f81b726612 | |||
| c55b8b6ed5 | |||
| ba55578143 | |||
| 95e270d9f3 | |||
| 0968932eff | |||
| 64302c9334 | |||
| 9d28ee483c | |||
| 4fa89879cf | |||
| 203e8aa23c | |||
| cc9e549aaf | |||
| fae2717a35 | |||
| cf8892b4cb | |||
| c8a394e0ce | |||
| 6bfed5a9e3 | |||
| a0df61a92d | |||
| bbf098e7f4 | |||
| 2c4ded0800 | |||
| f0e42287cf | |||
| 9ebb6c90d9 | |||
| 69d96d646d | |||
| c87ce26da8 | |||
| 3dea27b09f | |||
| 4d8632e26c | |||
| eee8ec42c5 | |||
| d7bca93af8 | |||
| 2b608b4725 | |||
| bd48b309ac | |||
| 15b843d46b |
@@ -6,22 +6,25 @@ This project is super lightweight by design. The UI is simple and the frontend i
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Clearnet Instances
|
||||
|
||||
| Instance URL | Region | Notes |
|
||||
| ------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| [code.whatever.social](https://code.whatever.social) | United States & Germany | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
|
||||
| [code.whatever.social](https://code.whatever.social) | United States & The Netherlands | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
|
||||
| [overflow.777.tf](https://overflow.777.tf/) | The Netherlands | Operated by [Jae](https://777.tf) |
|
||||
| [ao.vern.cc](https://ao.vern.cc) | United States | Operated by [vern.cc](https://vern.cc) |
|
||||
| [overflow.smnz.de](https://overflow.smnz.de) | Germany | Operated by [smnz.de](https://smnz.de) |
|
||||
| [anonymousoverflow.esmailelbob.xyz](https://anonymousoverflow.esmailelbob.xyz/) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
| [overflow.lunar.icu](https://overflow.lunar.icu) | Germany | Operated by [lunar.icu](https://lunar.icu/) |
|
||||
| [overflow.adminforge.de](https://overflow.adminforge.de/) | Germany | Operated by [adminForge](https://adminforge.de/) |
|
||||
| [ao.foss.wtf](https://ao.foss.wtf) | Germany | Operated by [foss.wtf](https://foss.wtf) |
|
||||
| [overflow.hostux.net](https://overflow.hostux.net/) | France | Operated by [Hostux](https://hostux.net/) |
|
||||
|
||||
## Other Instances
|
||||
|
||||
@@ -54,6 +57,12 @@ Their main website also [contains trackers from Alphabet](https://themarkup.org/
|
||||
|
||||
StackOverflow has a cluttered UI that might distract you from the content you're trying to find. AnonymousOverflow simplifies the interface to make it easier to read and navigate.
|
||||
|
||||
## How to make Stack Overflow links take you to AnonymousOverflow automatically
|
||||
|
||||
The open-source [Libredirect](https://github.com/libredirect/libredirect) extension for Firefox and Chromium-based desktop browsers has support for redirections to AnonymousOverflow. To enable this, simply open the extension settings, click on Stack Overflow, then toggle "Enable". That's it, now Stack Overflow links will go to AnonymousOverflow.
|
||||
|
||||
The open-source [FREEdirector](https://openuserjs.org/scripts/sjehuda/FREEdirector) user.js script for web browsers with userscript support. You can install it with a web extension like [Greasemonkey](https://greasespot.net/), [Tampermonkey](https://tampermonkey.net/) or [Violentmonkey](https://violentmonkey.github.io/). Once installed, Stack Overflow links will go to AnonymousOverflow.
|
||||
|
||||
## How it works
|
||||
|
||||
AnonymousOverflow uses the existing question endpoint that StackOverflow uses. Simply replace the domain name in the URL with the domain name of the AnonymousOverflow instance you're using and you'll be able to view the question anonymously.
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package config
|
||||
|
||||
var Version = "1.5"
|
||||
var Version = "1.8.1"
|
||||
|
||||
@@ -32,7 +32,6 @@ func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.LoadHTMLGlob("templates/*")
|
||||
r.Static("/static", "./public")
|
||||
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middleware.XssPreventionHeaders())
|
||||
@@ -40,6 +39,8 @@ func main() {
|
||||
r.Use(middleware.OptionsMiddleware())
|
||||
r.Use(middleware.Ratelimit())
|
||||
|
||||
r.GET("/static/*filepath", routes.StaticContent)
|
||||
|
||||
r.GET("/robots.txt", func(c *gin.Context) {
|
||||
c.String(200, "User-agent: *\nDisallow: /")
|
||||
})
|
||||
@@ -47,10 +48,26 @@ func main() {
|
||||
r.GET("/options/:name", routes.ChangeOptions)
|
||||
|
||||
r.GET("/", routes.GetHome)
|
||||
|
||||
r.POST("/", routes.PostHome)
|
||||
|
||||
r.GET("/questions/:id", func(c *gin.Context) {
|
||||
// redirect user to the question with the title
|
||||
c.Redirect(302, fmt.Sprintf("/questions/%s/placeholder", c.Param("id")))
|
||||
})
|
||||
r.GET("/questions/:id/:title", routes.ViewQuestion)
|
||||
r.GET("/questions/:id/:title/:answerId", func(c *gin.Context) {
|
||||
// redirect user to the answer with the title
|
||||
c.Redirect(302, fmt.Sprintf("/questions/%s/%s#%s", c.Param("id"), c.Param("title"), c.Param("answerId")))
|
||||
})
|
||||
r.GET("/exchange/:sub/questions/:id/:title", routes.ViewQuestion)
|
||||
r.GET("/exchange/:sub/questions/:id", func(c *gin.Context) {
|
||||
// redirect user to the question with the title
|
||||
c.Redirect(302, fmt.Sprintf("/exchange/%s/questions/%s/placeholder", c.Param("sub"), c.Param("id")))
|
||||
})
|
||||
r.GET("/exchange/:sub/questions/:id/:title/:answerId", func(c *gin.Context) {
|
||||
// redirect user to the answer with the title
|
||||
c.Redirect(302, fmt.Sprintf("/exchange/%s/questions/%s/%s#%s", c.Param("sub"), c.Param("id"), c.Param("title"), c.Param("answerId")))
|
||||
})
|
||||
|
||||
r.GET("/proxy", routes.GetImage)
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 491 B |
+85
-8
@@ -29,6 +29,20 @@ body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.card-tags .tag {
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--tag-text-color);
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--code-bg);
|
||||
padding: 0.15rem;
|
||||
@@ -45,27 +59,34 @@ pre {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.post-divider,
|
||||
.answer-divider {
|
||||
.post-divider {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.answer-divider {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answer {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--muted-text-color);
|
||||
}
|
||||
|
||||
.answer:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.answer-meta {
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
@@ -73,6 +94,14 @@ img {
|
||||
margin-bottom: 1rem;
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--text-color);
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.answer-meta p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.answer-meta.accepted {
|
||||
@@ -111,6 +140,54 @@ img {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.s-notice .d-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.s-notice .mr8 {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.s-notice svg {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.answers-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.answers-header .sorting form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.answers-header select {
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--text-color);
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.answers-header button {
|
||||
width: 2rem;
|
||||
border: 0;
|
||||
height: 2rem;
|
||||
border-radius: 5px;
|
||||
background-color: var(--meta-bg);
|
||||
color: var(--text-color);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
body {
|
||||
padding-left: 1rem;
|
||||
|
||||
@@ -2,6 +2,8 @@ package middleware
|
||||
|
||||
import (
|
||||
"anonymousoverflow/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +14,17 @@ var ipMap = sync.Map{}
|
||||
|
||||
func Ratelimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
if os.Getenv("DISABLE_RATELIMIT") == "true" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/static") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
ip := c.ClientIP()
|
||||
|
||||
// log request count as the value, ip as key
|
||||
|
||||
+16
-4
@@ -3,6 +3,7 @@ package routes
|
||||
import (
|
||||
"anonymousoverflow/config"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -19,6 +20,8 @@ type urlConversionRequest struct {
|
||||
URL string `form:"url" binding:"required"`
|
||||
}
|
||||
|
||||
var stackExchangeRegex = regexp.MustCompile(`https://(.+).stackexchange.com/questions/`)
|
||||
|
||||
func PostHome(c *gin.Context) {
|
||||
body := urlConversionRequest{}
|
||||
|
||||
@@ -33,15 +36,24 @@ func PostHome(c *gin.Context) {
|
||||
soLink := body.URL
|
||||
|
||||
// validate URL
|
||||
if !strings.HasPrefix(soLink, "https://stackoverflow.com/questions/") {
|
||||
isStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/questions/")
|
||||
isStackExchange := stackExchangeRegex.MatchString(soLink)
|
||||
if !isStackExchange && !isStackOverflow {
|
||||
c.HTML(400, "home.html", gin.H{
|
||||
"errorMessage": "Invalid stack overflow URL",
|
||||
"errorMessage": "Invalid stack overflow/exchange URL",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to the proxied thread
|
||||
c.Redirect(302, fmt.Sprintf("/questions/%s", strings.TrimPrefix(soLink, "https://stackoverflow.com/questions/")))
|
||||
// if stack overflow, trim https://stackoverflow.com
|
||||
if isStackOverflow {
|
||||
c.Redirect(302, strings.TrimPrefix(soLink, "https://stackoverflow.com"))
|
||||
return
|
||||
}
|
||||
|
||||
// if stack exchange, extract the subdomain
|
||||
sub := stackExchangeRegex.FindStringSubmatch(soLink)[1]
|
||||
|
||||
c.Redirect(302, fmt.Sprintf("/exchange/%s/%s", sub, strings.TrimPrefix(soLink, fmt.Sprintf("https://%s.stackexchange.com", sub))))
|
||||
}
|
||||
|
||||
+1
-12
@@ -19,12 +19,6 @@ func GetImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
url := c.Query("url")
|
||||
if url == "" {
|
||||
c.String(400, "Missing url")
|
||||
return
|
||||
}
|
||||
|
||||
// validate the auth token
|
||||
token, err := jwt.ParseWithClaims(authorization, &types.ImageProxyClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
|
||||
@@ -51,11 +45,6 @@ func GetImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.ImageURL != url {
|
||||
c.String(400, "Request & token mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Exp < time.Now().Unix() {
|
||||
c.String(400, "Token expired")
|
||||
return
|
||||
@@ -63,7 +52,7 @@ func GetImage(c *gin.Context) {
|
||||
|
||||
// download the image
|
||||
client := resty.New()
|
||||
resp, err := client.R().Get(url)
|
||||
resp, err := client.R().Get(claims.ImageURL)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(500)
|
||||
return
|
||||
|
||||
+65
-29
@@ -21,6 +21,13 @@ import (
|
||||
var codeBlockRegex = regexp.MustCompile(`(?s)<pre><code>(.+?)<\/code><\/pre>`)
|
||||
var questionCodeBlockRegex = regexp.MustCompile(`(?s)<pre class=".+"><code( class=".+")?>(.+?)</code></pre>`)
|
||||
|
||||
var soSortValues = map[string]string{
|
||||
"votes": "scoredesc",
|
||||
"trending": "trending",
|
||||
"newest": "modifieddesc",
|
||||
"oldest": "createdasc",
|
||||
}
|
||||
|
||||
func ViewQuestion(c *gin.Context) {
|
||||
client := resty.New()
|
||||
|
||||
@@ -36,7 +43,25 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
questionTitle := c.Param("title")
|
||||
|
||||
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s", questionId, questionTitle)
|
||||
sortValue := c.Query("sort_by")
|
||||
if sortValue == "" {
|
||||
sortValue = "votes"
|
||||
}
|
||||
|
||||
soSortValue, ok := soSortValues[sortValue]
|
||||
if !ok {
|
||||
soSortValue = soSortValues["votes"]
|
||||
}
|
||||
|
||||
sub := c.Param("sub")
|
||||
|
||||
domain := "stackoverflow.com"
|
||||
|
||||
if sub != "" {
|
||||
domain = fmt.Sprintf("%s.stackexchange.com", sub)
|
||||
}
|
||||
|
||||
soLink := fmt.Sprintf("https://%s/questions/%s/%s?answertab=%s", domain, questionId, questionTitle, soSortValue)
|
||||
|
||||
resp, err := client.R().Get(soLink)
|
||||
if err != nil {
|
||||
@@ -47,6 +72,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
defer resp.RawResponse.Body.Close()
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
c.HTML(500, "home.html", gin.H{
|
||||
@@ -81,6 +107,9 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
questionPostLayout := doc.Find("div.post-layout").First()
|
||||
|
||||
questionTags := utils.GetPostTags(questionPostLayout)
|
||||
newFilteredQuestion.Tags = questionTags
|
||||
|
||||
questionBodyParent := doc.Find("div.s-prose")
|
||||
|
||||
questionBodyParentHTML, err := questionBodyParent.Html()
|
||||
@@ -108,7 +137,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
newFilteredQuestion.ShortenedBody = shortenedBody
|
||||
|
||||
comments := utils.FindAndReturnComments(questionBodyParentHTML, questionPostLayout)
|
||||
comments := utils.FindAndReturnComments(questionBodyParentHTML, domain, questionPostLayout)
|
||||
newFilteredQuestion.Comments = comments
|
||||
|
||||
// parse any code blocks and highlight them
|
||||
@@ -149,24 +178,33 @@ func ViewQuestion(c *gin.Context) {
|
||||
userDetails := questionMetadata.Find("div.user-details")
|
||||
|
||||
questionAuthor := ""
|
||||
questionAuthorURL := ""
|
||||
questionAuthorURL := fmt.Sprintf("https://%s", domain)
|
||||
|
||||
// check if the question has been edited
|
||||
isQuestionEdited := false
|
||||
questionMetadata.Find("a.js-gps-track").Each(func(i int, s *goquery.Selection) {
|
||||
if strings.Contains(s.Text(), "edited") {
|
||||
isQuestionEdited = true
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
// if question has been edited, the author is the second element.
|
||||
|
||||
if isQuestionEdited {
|
||||
if i == 1 {
|
||||
questionAuthor = s.Text()
|
||||
questionAuthorURL, _ = s.Attr("href")
|
||||
questionAuthorURL += s.AttrOr("href", "")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// otherwise it's the first element
|
||||
if i == 0 {
|
||||
questionAuthor = s.Text()
|
||||
questionAuthorURL += s.AttrOr("href", "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise it's the second element
|
||||
if i == 1 {
|
||||
questionAuthor = s.Text()
|
||||
questionAuthorURL, _ = s.Attr("href")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
@@ -176,6 +214,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
answers := []types.FilteredAnswer{}
|
||||
|
||||
doc.Find("div.answer").Each(func(i int, s *goquery.Selection) {
|
||||
|
||||
newFilteredAnswer := types.FilteredAnswer{}
|
||||
|
||||
postLayout := s.Find("div.post-layout")
|
||||
@@ -191,30 +230,25 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
answerFooter := s.Find("div.mt24")
|
||||
|
||||
answerAuthorURL := ""
|
||||
answerAuthorURL := fmt.Sprintf("https://%s", domain)
|
||||
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() > 0 {
|
||||
questionAuthor := answerAuthorDetails.Find("a").First()
|
||||
answerAuthorName = html.EscapeString(questionAuthor.Text())
|
||||
answerAuthorURL += html.EscapeString(questionAuthor.AttrOr("href", ""))
|
||||
}
|
||||
|
||||
if answerAuthorDetails.Length() > 1 {
|
||||
if i == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
answerAuthor := answerAuthorDetails.Find("a").First()
|
||||
|
||||
answerAuthorURL = html.EscapeString(answerAuthor.AttrOr("href", ""))
|
||||
answerAuthorName = html.EscapeString(answerAuthor.Text())
|
||||
answerTimestamp = html.EscapeString(s.Find("span.relativetime").Text())
|
||||
})
|
||||
|
||||
answerId, _ := s.Attr("data-answerid")
|
||||
newFilteredAnswer.ID = answerId
|
||||
|
||||
newFilteredAnswer.AuthorName = answerAuthorName
|
||||
newFilteredAnswer.AuthorURL = answerAuthorURL
|
||||
newFilteredAnswer.Timestamp = answerTimestamp
|
||||
@@ -231,7 +265,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
answerBodyHTML = strings.Replace(answerBodyHTML, codeBlock, highlightedCodeBlock, 1)
|
||||
}
|
||||
|
||||
comments = utils.FindAndReturnComments(answerBodyHTML, postLayout)
|
||||
comments = utils.FindAndReturnComments(answerBodyHTML, domain, postLayout)
|
||||
|
||||
newFilteredAnswer.Comments = comments
|
||||
newFilteredAnswer.Body = template.HTML(utils.ReplaceImgTags(answerBodyHTML))
|
||||
@@ -251,6 +285,8 @@ func ViewQuestion(c *gin.Context) {
|
||||
"imagePolicy": imagePolicy,
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"currentUrl": fmt.Sprintf("%s/questions/%s/%s", os.Getenv("APP_URL"), questionId, questionTitle),
|
||||
"sortValue": sortValue,
|
||||
"domain": domain,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func StaticContent(c *gin.Context) {
|
||||
cleanFilePath := strings.ReplaceAll(c.Param("filepath"), "..", "")
|
||||
|
||||
c.File(fmt.Sprintf("./public/%s", cleanFilePath))
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package types
|
||||
import "html/template"
|
||||
|
||||
type FilteredAnswer struct {
|
||||
ID string
|
||||
|
||||
Upvotes string
|
||||
IsAccepted bool
|
||||
|
||||
|
||||
@@ -10,4 +10,5 @@ type FilteredQuestion struct {
|
||||
AuthorURL string
|
||||
ShortenedBody string
|
||||
Comments []FilteredComment
|
||||
Tags []string
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package utils
|
||||
|
||||
import (
|
||||
"anonymousoverflow/src/types"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func FindAndReturnComments(inHtml string, postLayout *goquery.Selection) (comments []types.FilteredComment) {
|
||||
func FindAndReturnComments(inHtml, domain string, postLayout *goquery.Selection) (comments []types.FilteredComment) {
|
||||
|
||||
commentsComponent := postLayout.Find("div.js-post-comments-component")
|
||||
|
||||
@@ -34,7 +35,7 @@ func FindAndReturnComments(inHtml string, postLayout *goquery.Selection) (commen
|
||||
return
|
||||
}
|
||||
|
||||
commentAuthorURL := ""
|
||||
commentAuthorURL := fmt.Sprintf("https://%s", domain)
|
||||
|
||||
commentAuthor := commentBody.Find("span.comment-user")
|
||||
if commentAuthor.Length() == 0 {
|
||||
@@ -43,7 +44,7 @@ func FindAndReturnComments(inHtml string, postLayout *goquery.Selection) (commen
|
||||
return
|
||||
}
|
||||
|
||||
commentAuthorURL = commentAuthor.AttrOr("href", "")
|
||||
commentAuthorURL += commentAuthor.AttrOr("href", "")
|
||||
}
|
||||
|
||||
commentTimestamp := commentBody.Find("span.relativetime-clean").Text()
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ func ReplaceImgTags(inHtml string) string {
|
||||
authToken, _ := generateImageProxyAuth(src)
|
||||
|
||||
// replace the img tag with a proxied url
|
||||
inHtml = strings.Replace(inHtml, imgTag, fmt.Sprintf(`<img src="%s/proxy?url=%s&auth=%s">`, os.Getenv("APP_URL"), src, authToken), 1)
|
||||
inHtml = strings.Replace(inHtml, imgTag, fmt.Sprintf(`<img src="%s/proxy?auth=%s">`, os.Getenv("APP_URL"), authToken), 1)
|
||||
}
|
||||
|
||||
return inHtml
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func GetPostTags(postLayout *goquery.Selection) []string {
|
||||
var tags []string
|
||||
postLayout.Find("a.post-tag").Each(func(i int, s *goquery.Selection) {
|
||||
tags = append(tags, s.Text())
|
||||
})
|
||||
return tags
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="comment-author">
|
||||
Commented {{ $comment.Timestamp }} by
|
||||
<a
|
||||
href="https://stackoverflow.com{{ $comment.AuthorURL }}"
|
||||
href="{{ $comment.AuthorURL }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $comment.AuthorName }}</a
|
||||
|
||||
+39
-12
@@ -30,7 +30,7 @@
|
||||
<p class="timestamp">
|
||||
Asked {{ .question.Timestamp }} by
|
||||
<a
|
||||
href="https://stackoverflow.com{{ .question.AuthorURL }}"
|
||||
href="{{ .question.AuthorURL }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ .question.AuthorName }}</a
|
||||
@@ -38,38 +38,65 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-body">{{ .question.Body }}</div>
|
||||
{{ if .question.Comments }}
|
||||
{{ template "comments.html" .question }}
|
||||
{{end}}
|
||||
|
||||
<div class="card-tags">
|
||||
{{ range .question.Tags }}
|
||||
<div class="tag">{{ . }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .question.Comments }} {{ template "comments.html"
|
||||
.question }} {{end}}
|
||||
</div>
|
||||
<hr class="post-divider" />
|
||||
<h2>Answers</h2>
|
||||
<div class="answers-header">
|
||||
<h2>Answers</h2>
|
||||
<div class="sorting">
|
||||
<form>
|
||||
<select name="sort_by">
|
||||
<option disabled value="">Sort answers by...</option>
|
||||
<option value="votes"{{ if eq .sortValue "votes" }} selected{{ end }}>Votes</option>
|
||||
<option value="trending"{{ if eq .sortValue "trending" }} selected{{ end }}>Trending</option>
|
||||
<option value="newest"{{ if eq .sortValue "newest" }} selected{{ end }}>Date modified (newest first)</option>
|
||||
<option value="oldest"{{ if eq .sortValue "oldest" }} selected{{ end }}>Date created (oldest first)</option>
|
||||
</select>
|
||||
<button type="submit">
|
||||
<img
|
||||
src="/static/icons/sort.svg"
|
||||
alt="Sieve icon"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ range $answer := .answers }}
|
||||
<div class="answer">
|
||||
<div class="answer" id="{{ $answer.ID }}">
|
||||
<div
|
||||
class="answer-meta{{ if $answer.IsAccepted }} accepted{{end}}"
|
||||
>
|
||||
<p>
|
||||
{{ if $answer.IsAccepted }} Accepted - {{ end }}
|
||||
{{$answer.Upvotes}} Votes
|
||||
</div>
|
||||
</p>
|
||||
<a href="#{{ $answer.ID }}" class="answer-link">
|
||||
<div class="icon">
|
||||
<img src="/static/icons/link.svg" alt="Paperclip icon" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{ $answer.Body }}
|
||||
<div class="answer-author-parent">
|
||||
<div class="answer-author">
|
||||
Answered {{ $answer.Timestamp }} by
|
||||
<a
|
||||
href="https://stackoverflow.com{{ $answer.AuthorURL }}"
|
||||
href="{{ $answer.AuthorURL }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $answer.AuthorName }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{{ if $answer.Comments }}
|
||||
{{ template "comments.html" $answer }}
|
||||
{{ if $answer.Comments }} {{ template "comments.html" $answer }}
|
||||
{{end}}
|
||||
</div>
|
||||
<hr class="answer-divider" />
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user