Compare commits

..

28 Commits

Author SHA1 Message Date
httpjamesm bdca47a51e Merge pull request #23 from r7l/main
style: Fix notice box styles
2023-03-15 13:32:30 -04:00
r7l 51190f8d69 Fix notice box styles 2023-03-02 19:59:45 +01:00
httpjamesm bfa19bc2d2 Merge pull request #21 from sjehuda/patch-1
docs: Add Userscript
2023-03-02 10:18:34 -05:00
Schimon Jehuda 55b06b986b Minor improvement
Improve English
2023-03-02 17:16:48 +02:00
Schimon Jehuda 09de9f11a5 Update README.md 2023-03-02 17:14:12 +02:00
Schimon Jehuda 63e9792d2a Add Userscript
Userscript for web browsers without a redirection addon
2023-03-02 17:02:07 +02:00
httpjamesm 8ee1d76993 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2023-02-25 12:39:11 -05:00
httpjamesm 6b77c5dae4 feat: DISABLE_RATELIMIT env 2023-02-25 12:39:09 -05:00
httpjamesm df93607e5d Merge pull request #19 from privacytime101/main
Add Libredirect to README
2023-02-22 20:55:00 -05:00
Whatever Social 05fbd9608e Add Libredirect to README 2023-02-20 13:21:27 -08:00
httpjamesm 66db070fa5 Merge pull request #18 from privacytime101/main
Update whatever.social region
2023-02-20 15:53:29 -05:00
Whatever Social bb9f8e2aa2 Update whatever.social region 2023-02-20 12:52:41 -08:00
httpjamesm 487a62e7aa chore: version 1.8 2023-02-20 14:23:42 -05:00
httpjamesm abfd0cfb6f feat: SO/SE converter
refactor: rework some author logic
2023-02-20 14:17:24 -05:00
httpjamesm 63a30ab1cf feat: don't count static toward ratelimit 2023-02-20 13:56:44 -05:00
httpjamesm f1370c29b6 fix: pass down domain to comment for author URLs 2023-02-20 13:54:09 -05:00
httpjamesm d935ce72f6 fix: pass domain to question template 2023-02-20 13:49:54 -05:00
httpjamesm 45644a0503 feat: get stackexchange posts
fix: comment author distinguishing
2023-02-16 10:25:24 -05:00
httpjamesm f81b726612 chore: version 1.7.1 2023-02-14 20:25:26 -05:00
httpjamesm c55b8b6ed5 fix: redeclare correct answer author vars 2023-02-14 20:25:16 -05:00
httpjamesm ba55578143 chore: version 1.7 2023-02-14 20:17:28 -05:00
httpjamesm 95e270d9f3 style: center align answer meta 2023-02-14 20:16:55 -05:00
httpjamesm 0968932eff fix: defer close raw response body 2023-02-14 20:16:30 -05:00
httpjamesm 64302c9334 refactor: simplify author parsing logic 2023-02-14 20:13:24 -05:00
httpjamesm 9d28ee483c Merge branch 'answer-linking' 2023-02-14 20:12:56 -05:00
httpjamesm 4fa89879cf feat: permalink generator 2023-02-14 20:12:49 -05:00
httpjamesm 203e8aa23c Merge branch 'answer-linking' 2023-02-14 20:10:06 -05:00
httpjamesm cc9e549aaf feat: get answer ID and fragment scrolling 2023-02-14 19:55:17 -05:00
12 changed files with 135 additions and 43 deletions
+7 -1
View File
@@ -16,7 +16,7 @@ This project is super lightweight by design. The UI is simple and the frontend i
| 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) |
@@ -57,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
View File
@@ -1,3 +1,3 @@
package config
var Version = "1.6.1"
var Version = "1.8.1"
+13
View File
@@ -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)
+3
View File
@@ -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

+20
View File
@@ -94,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 {
@@ -132,6 +140,18 @@ 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;
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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,
})
}
+2
View File
@@ -3,6 +3,8 @@ package types
import "html/template"
type FilteredAnswer struct {
ID string
Upvotes string
IsAccepted bool
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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