mirror of
https://github.com/httpjamesm/AnonymousOverflow.git
synced 2024-12-06 19:16:43 +01:00
Compare commits
23 Commits
v1.6
...
stackexchange
| Author | SHA1 | Date | |
|---|---|---|---|
| 487a62e7aa | |||
| abfd0cfb6f | |||
| 63a30ab1cf | |||
| f1370c29b6 | |||
| d935ce72f6 | |||
| 45644a0503 | |||
| f81b726612 | |||
| c55b8b6ed5 | |||
| ba55578143 | |||
| 95e270d9f3 | |||
| 0968932eff | |||
| 64302c9334 | |||
| 9d28ee483c | |||
| 4fa89879cf | |||
| 203e8aa23c | |||
| cc9e549aaf | |||
| fae2717a35 | |||
| cf8892b4cb | |||
| c8a394e0ce | |||
| 6bfed5a9e3 | |||
| a0df61a92d | |||
| bbf098e7f4 | |||
| 2c4ded0800 |
@@ -22,6 +22,9 @@ This project is super lightweight by design. The UI is simple and the frontend i
|
||||
| [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
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package config
|
||||
|
||||
var Version = "1.6"
|
||||
var Version = "1.8"
|
||||
|
||||
@@ -55,6 +55,19 @@ func main() {
|
||||
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 |
@@ -59,6 +59,10 @@ pre {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.8rem;
|
||||
@@ -90,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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"anonymousoverflow/config"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +13,12 @@ var ipMap = sync.Map{}
|
||||
|
||||
func Ratelimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
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))))
|
||||
}
|
||||
|
||||
+44
-29
@@ -53,7 +53,15 @@ func ViewQuestion(c *gin.Context) {
|
||||
soSortValue = soSortValues["votes"]
|
||||
}
|
||||
|
||||
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s?answertab=%s", questionId, questionTitle, soSortValue)
|
||||
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 {
|
||||
@@ -64,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{
|
||||
@@ -128,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
|
||||
@@ -169,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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -196,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")
|
||||
@@ -211,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
|
||||
@@ -251,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))
|
||||
@@ -272,6 +286,7 @@ func ViewQuestion(c *gin.Context) {
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"currentUrl": fmt.Sprintf("%s/questions/%s/%s", os.Getenv("APP_URL"), questionId, questionTitle),
|
||||
"sortValue": sortValue,
|
||||
"domain": domain,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package types
|
||||
import "html/template"
|
||||
|
||||
type FilteredAnswer struct {
|
||||
ID string
|
||||
|
||||
Upvotes string
|
||||
IsAccepted bool
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-4
@@ -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
|
||||
@@ -68,19 +68,26 @@
|
||||
</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
|
||||
|
||||
Reference in New Issue
Block a user