Compare commits

...

19 Commits

Author SHA1 Message Date
httpjamesm 15b843d46b fix: remove ?url for image proxy 2023-02-02 19:15:55 -05:00
httpjamesm 7b80c55dd2 chore: version 1.5 2023-02-02 19:06:19 -05:00
httpjamesm 12174b2600 chore: update docker compose example 2023-02-02 19:05:37 -05:00
httpjamesm 5f11bcd6a2 feat!: proxy images with JWT auth 2023-02-02 19:05:08 -05:00
httpjamesm 7494b2df33 docs: lunar.icu instance 2023-01-14 15:03:08 -05:00
httpjamesm 517a9afb89 Merge pull request #8 from EsmailELBoBDev2/patch-1
added my instance
2023-01-12 07:06:18 -05:00
Esmail EL BoB f9e42ca8c7 added my instance 2023-01-12 04:23:46 +02:00
httpjamesm 3aac1467cd feat: version 1.4 2023-01-01 00:16:05 -05:00
httpjamesm 062ffbcbdc refactor: move comments styles to another file 2022-12-30 18:18:13 -05:00
httpjamesm aa13c8515f feat: change comments to plural if greater than 1 2022-12-30 18:15:35 -05:00
httpjamesm d49c0f2ffd feat: display comment upvotes 2022-12-30 18:11:59 -05:00
httpjamesm 89d8dee958 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2022-12-30 10:53:27 -05:00
httpjamesm 0ad3f08ef5 docs: add smnz.de 2022-12-30 10:53:26 -05:00
httpjamesm d5bcf7fa38 docs: link deployment wiki page 2022-12-29 17:14:14 -05:00
httpjamesm 95df8a715b feat: version 1.3.4 2022-12-29 14:54:13 -05:00
httpjamesm f2b369b2c6 fix: remove length requirement 2022-12-29 14:54:05 -05:00
httpjamesm 17a8c6b1e1 feat: version 1.3.3 2022-12-29 14:47:16 -05:00
httpjamesm be9380ba20 fix: allow backwards compatibility with older threads 2022-12-29 14:47:07 -05:00
httpjamesm e4d429b642 fix: set action to / for conversion 2022-12-29 14:46:47 -05:00
18 changed files with 220 additions and 62 deletions
+18 -20
View File
@@ -14,22 +14,26 @@ This project is super lightweight by design. The UI is simple and the frontend i
## 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) |
| [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) |
| 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) |
| [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/) |
## Other Instances
| Instance URL | Region | Notes |
| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------- |
| [ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | United States | Operated by [vern.cc](https://vern.cc) |
| [vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p](http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p) | United States | Operated by [vern.cc](https://vern.cc) |
| Instance URL | Region | Notes |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------- |
| [ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | United States | Operated by [vern.cc](https://vern.cc) |
| [vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p](http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p) | United States | Operated by [vern.cc](https://vern.cc) |
| [anonymousoverflow.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://anonymousoverflow.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
## Why use AnonymousOverflow over StackOverflow?
- StackOverflow collects a lot of information
- StackOverflow collects a lot of information
While it's understandable that StackOverflow collects a lot of technical data for product development, debugging and to serve the best experience to its users, not everyone wants their
@@ -37,16 +41,16 @@ While it's understandable that StackOverflow collects a lot of technical data fo
to be collected and stored.
- StackOverflow shares your information with third-parties
- StackOverflow shares your information with third-parties
StackOverflow does not sell your information, but it does share it with third-parties, including conglomerates.
> We also partner with other third parties, such as Google Ads and Microsoft Bing, to serve advertising content and manage advertising campaigns. When we use Google Ads or Microsoft Bing Customer Match for advertising campaigns, your personal data will be protected using hashed codes.
Google users can control the ads that they see on Google services, including Customer Match ads, in their Google Ads Settings.
> Google users can control the ads that they see on Google services, including Customer Match ads, in their Google Ads Settings.
Their main website also [contains trackers from Alphabet](https://themarkup.org/blacklight?url=stackoverflow.com).
- Reduced clutter
- Reduced clutter
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.
@@ -86,13 +90,7 @@ Thanks to [Manav from ente.io](https://ente.io/about) for the handy tool.
## How to deploy
AnonymousOverflow uses Docker for deployment.
Install Docker for your platform and copy the `docker-compose.example.yml` file to `docker-compose.yml`.
Then, tweak settings if needed.
Run `docker compose up -d` to build and start the container.
Read the [wiki page](https://github.com/httpjamesm/AnonymousOverflow/wiki/Deployment).
## Attribution
+1 -1
View File
@@ -1,3 +1,3 @@
package config
var Version = "1.3.2"
var Version = "1.5.1"
+1
View File
@@ -8,6 +8,7 @@ services:
network: 'host'
environment:
- APP_URL=https://domain.com
- JWT_SIGNING_SECRET=secret
ports:
- '80:8080'
restart: 'always'
+1
View File
@@ -10,6 +10,7 @@ import (
func RunChecks() {
godotenv.Load(".env")
checkEnv("APP_URL")
checkEnv("JWT_SIGNING_SECRET")
}
func checkEnv(key string) {
+1
View File
@@ -17,6 +17,7 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
+2
View File
@@ -27,6 +27,8 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPr
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+2
View File
@@ -52,5 +52,7 @@ func main() {
r.GET("/questions/:id/:title", routes.ViewQuestion)
r.GET("/proxy", routes.GetImage)
r.Run(fmt.Sprintf("%s:%s", host, port))
}
+35
View File
@@ -0,0 +1,35 @@
.comments {
margin-top: 1rem;
}
.comments-parent {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.comment-parent {
cursor: text;
}
.comment-top {
display: flex;
}
.comment-score {
border-right: 1px solid white;
padding-right: .5rem;
}
.comment-body {
font-size: 0.95rem;
padding-left: .5rem;
}
.comment-author {
color: var(--muted-text-color);
font-size: 0.8rem;
padding-left: 1.5rem;
margin-top: .25rem;
}
+2 -25
View File
@@ -50,7 +50,8 @@ pre {
font-size: 0.8rem;
}
.post-divider, .answer-divider {
.post-divider,
.answer-divider {
margin-top: 3rem;
margin-bottom: 3rem;
}
@@ -102,30 +103,6 @@ img {
height: 2rem;
}
.comments {
margin-top: 1rem;
}
.comments-parent {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.comment-parent {
cursor: text;
}
.comment-body {
font-size: 0.95rem;
}
.comment-author {
color: var(--muted-text-color);
font-size: 0.8rem;
}
.s-notice {
background-color: var(--meta-bg);
padding: 1rem;
+66
View File
@@ -0,0 +1,66 @@
package routes
import (
"anonymousoverflow/src/types"
"fmt"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
"github.com/golang-jwt/jwt/v4"
)
func GetImage(c *gin.Context) {
authorization := c.Query("auth")
if authorization == "" {
c.String(400, "Missing auth token")
return
}
// validate the auth token
token, err := jwt.ParseWithClaims(authorization, &types.ImageProxyClaims{}, func(token *jwt.Token) (interface{}, error) {
// validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SIGNING_SECRET")), nil
})
if err != nil {
c.String(400, err.Error())
return
}
claims, ok := token.Claims.(*types.ImageProxyClaims)
if !ok || !token.Valid {
c.String(400, "Invalid token")
return
}
if claims.Action != "imageProxy" {
c.String(400, "Invalid action")
return
}
if claims.Exp < time.Now().Unix() {
c.String(400, "Token expired")
return
}
// download the image
client := resty.New()
resp, err := client.R().Get(claims.ImageURL)
if err != nil {
c.AbortWithStatus(500)
return
}
// set the content type
c.Header("Content-Type", resp.Header().Get("Content-Type"))
// write the image to the response
c.Writer.Write(resp.Body())
}
+2 -11
View File
@@ -25,15 +25,6 @@ func ViewQuestion(c *gin.Context) {
client := resty.New()
questionId := c.Param("id")
if len(questionId) < 8 {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid question ID",
"theme": c.MustGet("theme").(string),
"version": config.Version,
})
return
}
if _, err := strconv.Atoi(questionId); err != nil {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid question ID",
@@ -102,7 +93,7 @@ func ViewQuestion(c *gin.Context) {
return
}
newFilteredQuestion.Body = template.HTML(questionBodyParentHTML)
newFilteredQuestion.Body = template.HTML(utils.ReplaceImgTags(questionBodyParentHTML))
questionBodyText := questionBodyParent.Text()
@@ -243,7 +234,7 @@ func ViewQuestion(c *gin.Context) {
comments = utils.FindAndReturnComments(answerBodyHTML, postLayout)
newFilteredAnswer.Comments = comments
newFilteredAnswer.Body = template.HTML(answerBodyHTML)
newFilteredAnswer.Body = template.HTML(utils.ReplaceImgTags(answerBodyHTML))
answers = append(answers, newFilteredAnswer)
})
+1
View File
@@ -7,4 +7,5 @@ type FilteredComment struct {
Timestamp string
AuthorName string
AuthorURL string
Upvotes string
}
+14
View File
@@ -0,0 +1,14 @@
package types
import "github.com/golang-jwt/jwt/v4"
type ImageProxyClaims struct {
Action string `json:"action"`
ImageURL string `json:"image_url"`
Iss int64 `json:"iss"`
Exp int64 `json:"exp"`
jwt.RegisteredClaims
}
+9
View File
@@ -17,6 +17,14 @@ func FindAndReturnComments(inHtml string, postLayout *goquery.Selection) (commen
allComments := commentsList2.Find("li.comment")
allComments.Each(func(i int, s *goquery.Selection) {
commentScoreParent := s.Find("div.comment-score")
// find the first span within commentScoreParent
commentScore := commentScoreParent.Find("span").First().Text()
if commentScore == "" {
commentScore = "0"
}
commentText := s.Find("div.comment-text")
commentBody := commentText.Find("div.comment-body")
@@ -45,6 +53,7 @@ func FindAndReturnComments(inHtml string, postLayout *goquery.Selection) (commen
Timestamp: commentTimestamp,
AuthorName: commentAuthor.Text(),
AuthorURL: commentAuthorURL,
Upvotes: commentScore,
}
comments = append(comments, newFilteredComment)
+52
View File
@@ -0,0 +1,52 @@
package utils
import (
"anonymousoverflow/src/types"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
)
var imgTagRegex = regexp.MustCompile(`<img[^>]*\s+src\s*=\s*"(.*?)"[^>]*>`)
func ReplaceImgTags(inHtml string) string {
// find all img tags
imgTags := imgTagRegex.FindAllString(inHtml, -1)
for _, imgTag := range imgTags {
// parse the src="" attribute
srcRegex := regexp.MustCompile(`src\s*=\s*"(.*?)"`)
src := srcRegex.FindStringSubmatch(imgTag)[1]
authToken, _ := generateImageProxyAuth(src)
// replace the img tag with a proxied url
inHtml = strings.Replace(inHtml, imgTag, fmt.Sprintf(`<img src="%s/proxy?auth=%s">`, os.Getenv("APP_URL"), authToken), 1)
}
return inHtml
}
func generateImageProxyAuth(url string) (string, error) {
// generate a jwt with types.ImageProxyClaims
claims := types.ImageProxyClaims{
Action: "imageProxy",
ImageURL: url,
Iss: time.Now().Unix(),
Exp: time.Now().Add(time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
// sign the token
ss, err := token.SignedString([]byte(os.Getenv("JWT_SIGNING_SECRET")))
if err != nil {
return "", err
}
return ss, nil
}
+10 -3
View File
@@ -1,13 +1,20 @@
<details class="comments">
<summary>
Show <b>{{ (len .Comments) }} comments</b>
Show
<b
>{{ (len .Comments) }} comment{{ if gt (len .Comments) 1 }}s{{
end }}</b
>
</summary>
<div class="comments-parent">
{{ range $comment := .Comments }}
<div class="comment-parent">
<div class="comment">
<div class="comment-body">
{{ $comment.Text }}
<div class="comment-top">
<div class="comment-score" title="Comment Upvotes">
{{ $comment.Upvotes }}
</div>
<div class="comment-body">{{ $comment.Text }}</div>
</div>
<div class="comment-author">
Commented {{ $comment.Timestamp }} by
+1 -1
View File
@@ -40,7 +40,7 @@
<p><b>Error</b>: {{ .errorMessage }}</p>
</div>
{{end}} {{ end }}
<form method="POST">
<form method="POST" action="/">
<div class="view-form">
<input
class="view-input"
+2 -1
View File
@@ -4,6 +4,7 @@
<title>{{ .question.Title }}</title>
<link rel="stylesheet" href="/static/question.css" />
<link rel="stylesheet" href="/static/syntax.css" />
<link rel="stylesheet" href="/static/comments.css" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'self'; script-src 'none'; img-src {{ .imagePolicy }};"
@@ -21,7 +22,7 @@
alt="4 circles with alternating colors between green and white"
/>
</a>
{{ template "themeSwitcher.html" .}}
{{ template "themeSwitcher.html" . }}
</div>
<div class="card">
<div class="card-header">