Compare commits

...

46 Commits

Author SHA1 Message Date
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
httpjamesm fae2717a35 chore: version 1.6.1 2023-02-14 19:41:51 -05:00
httpjamesm cf8892b4cb Merge pull request #16 from r-flash/patch-1
fix: Remove inline CSS backgrounds in a code block
2023-02-13 12:39:17 -05:00
Adam P c8a394e0ce Remove inline CSS backgrounds in a code block
The lines in a `<pre>` code block may slightly overlap. Lower parts of a line can thus be obcured by the next one. This PR removes the background of `<code>` when it's inside a `<pre>` which already has the same  background color set.
2023-02-13 18:33:01 +01:00
httpjamesm 6bfed5a9e3 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2023-02-12 12:28:48 -05:00
httpjamesm a0df61a92d docs: add ao.foss.wtf and overflow.hostux.net 2023-02-12 12:28:19 -05:00
httpjamesm bbf098e7f4 Merge pull request #13 from privacytime101/main
docs: add adminforge.de
2023-02-05 14:27:24 -05:00
Whatever Social 2c4ded0800 Add adminforge.de 2023-02-05 11:01:37 -08:00
httpjamesm f0e42287cf chore: version 1.6 2023-02-04 13:42:57 -05:00
httpjamesm 9ebb6c90d9 fix: sort names 2023-02-04 13:42:24 -05:00
httpjamesm 69d96d646d feat: question tags 2023-02-04 13:40:57 -05:00
httpjamesm c87ce26da8 style: use border-bottom and disable border on last-child 2023-02-04 13:18:40 -05:00
httpjamesm 3dea27b09f Merge branch 'sorting' 2023-02-04 13:16:33 -05:00
httpjamesm 4d8632e26c fix: add no cache to static 2023-02-04 13:14:52 -05:00
httpjamesm eee8ec42c5 feat: hashmap with SO sort values, ssr selection 2023-02-04 13:05:10 -05:00
httpjamesm d7bca93af8 feat: sort select with button 2023-02-04 12:58:50 -05:00
httpjamesm 2b608b4725 feat: redirect questions without title in URL 2023-02-04 12:49:28 -05:00
httpjamesm bd48b309ac docs: update screenshot CDN links 2023-02-03 17:53:59 -05:00
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
24 changed files with 412 additions and 97 deletions
+24 -23
View File
@@ -6,30 +6,37 @@ This project is super lightweight by design. The UI is simple and the frontend i
## Screenshots
![Home](https://cdn.horizon.pics/7HbXKZ3JBJbBDMtt51DVJgo5THFMYX.png)
![Home](https://files.horizon.pics/e2b9275c-1409-4978-801b-de981a8d3ae9?a=1&mime1=image&mime2=png)
![Question](https://cdn.horizon.pics/lqEuJMzYpgeB8au4QASsZ0S0BQHknJ.png)
![Question](https://files.horizon.pics/0f6b0036-87f0-4acd-9a0f-936b5c397a73?a=1&mime1=image&mime2=png)
![Answer](https://cdn.horizon.pics/0RwQtrxH3COTC9sfo0FsQ56I3Opl0m.png)
![Answer](https://files.horizon.pics/861ec510-644b-43f2-9439-0a2cae841422?a=1&mime1=image&mime2=png)
## 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/) |
| [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
| 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 +44,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 +93,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.7.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=
+12 -2
View File
@@ -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,19 @@ 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("/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;
}
+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

+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="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

+73 -31
View File
@@ -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,26 +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;
@@ -72,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 {
@@ -102,30 +132,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;
@@ -134,6 +140,42 @@ img {
margin-bottom: 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;
+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())
}
+34 -25
View File
@@ -21,19 +21,17 @@ 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()
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",
@@ -45,7 +43,17 @@ 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"]
}
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s?answertab=%s", questionId, questionTitle, soSortValue)
resp, err := client.R().Get(soLink)
if err != nil {
@@ -56,6 +64,7 @@ func ViewQuestion(c *gin.Context) {
})
return
}
defer resp.RawResponse.Body.Close()
if resp.StatusCode() != 200 {
c.HTML(500, "home.html", gin.H{
@@ -90,6 +99,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()
@@ -102,7 +114,7 @@ func ViewQuestion(c *gin.Context) {
return
}
newFilteredQuestion.Body = template.HTML(questionBodyParentHTML)
newFilteredQuestion.Body = template.HTML(utils.ReplaceImgTags(questionBodyParentHTML))
questionBodyText := questionBodyParent.Text()
@@ -185,6 +197,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")
@@ -205,25 +218,20 @@ func ViewQuestion(c *gin.Context) {
answerTimestamp := ""
answerFooter.Find("div.post-signature").Each(func(i int, s *goquery.Selection) {
answerAuthorDetails := s.Find("div.user-details")
questionAuthorDetails := questionMetadata.Find("div.user-details")
if answerAuthorDetails.Length() == 0 {
return
if questionAuthorDetails.Length() > 0 {
questionAuthor := questionAuthorDetails.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
@@ -243,7 +251,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)
})
@@ -260,6 +268,7 @@ 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,
})
}
+14
View File
@@ -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))
}
+2
View File
@@ -3,6 +3,8 @@ package types
import "html/template"
type FilteredAnswer struct {
ID string
Upvotes string
IsAccepted bool
+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
}
+1
View File
@@ -10,4 +10,5 @@ type FilteredQuestion struct {
AuthorURL string
ShortenedBody string
Comments []FilteredComment
Tags []string
}
+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
}
+13
View File
@@ -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
}
+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"
+39 -11
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">
@@ -37,21 +38,50 @@
</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">
@@ -64,11 +94,9 @@
>
</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>