Compare commits

...

77 Commits

Author SHA1 Message Date
httpjamesm 72a7392142 Merge branch 'main' into concurrency-improvements 2023-08-21 01:08:06 -04:00
httpjamesm 4760681c55 Merge pull request #52 from httpjamesm:air-toml
Air-toml
2023-08-21 01:07:52 -04:00
httpjamesm 6495e4a4bd fix: remove . 2023-08-21 01:07:32 -04:00
httpjamesm 1f77bf4ed6 Merge branch 'main' into concurrency-improvements 2023-08-21 01:07:02 -04:00
httpjamesm 9a6a044d20 Merge branch 'main' into air-toml 2023-08-21 01:06:24 -04:00
httpjamesm 7201269609 air toml config with tmp ignore (#51) 2023-08-21 01:05:08 -04:00
httpjamesm a666e02256 air toml config with tmp ignore 2023-08-21 01:04:48 -04:00
httpjamesm 50fc162b47 refactor: concurrently proxy img tags 2023-08-21 01:04:11 -04:00
Arya K 2fe61471fb Update location of Project Segfault instances (#49) 2023-08-21 00:42:22 -04:00
httpjamesm 4e8a7d3553 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2023-07-18 22:34:38 -04:00
httpjamesm 2bbed5606d docs: new instances 2023-07-18 22:34:37 -04:00
httpjamesm dbbc9570db version 1.8.3 2023-07-05 12:51:10 -04:00
Arya K 37d28dd704 Add AnonymousOverflow to question title (#39)
This patch adds anonymousoverflow to the title of questions, making tracking anonymousoverflow through things like activitywatch easier..
2023-07-05 12:50:26 -04:00
httpjamesm 236eb849e4 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2023-06-08 16:37:49 -04:00
httpjamesm ee5f4eb4b6 docs: bloatcat instance 2023-06-08 16:37:47 -04:00
httpjamesm 975ed51726 Merge pull request #36 from httpjamesm/dependabot/go_modules/golang.org/x/net-0.7.0
chore(deps): bump golang.org/x/net from 0.4.0 to 0.7.0
2023-05-30 09:09:47 -04:00
dependabot[bot] 5b8e6acf9e chore(deps): bump golang.org/x/net from 0.4.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.4.0 to 0.7.0.
- [Commits](https://github.com/golang/net/compare/v0.4.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-28 14:25:42 +00:00
httpjamesm 92b8707882 docs: remove esmail, 777, foss.wtf 2023-05-21 21:45:33 -04:00
httpjamesm c95c2d74b3 docs: add fascinated.cc instance 2023-05-01 08:58:40 -04:00
httpjamesm 296b30323a docs: xbdm.fun instance 2023-04-16 16:31:08 -04:00
httpjamesm 225420029e docs: project segfault onion instance 2023-04-08 12:32:44 -04:00
httpjamesm 6736b6440c docs: add link to instance URL 2023-04-02 11:36:49 -04:00
httpjamesm 7dc9856643 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2023-04-02 11:36:14 -04:00
httpjamesm 5bc43af96d docs: project segfault instance 2023-04-02 11:36:12 -04:00
httpjamesm 2afecd8ae7 feat: version 1.8.2 2023-03-15 14:33:48 -03:00
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
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
25 changed files with 530 additions and 98 deletions
+44
View File
@@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
+2 -1
View File
@@ -1,4 +1,5 @@
.env
docker-compose.yml
.DS_Store
*bin
*bin
/tmp
+44 -24
View File
@@ -6,31 +6,45 @@ 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) |
| [overflow.smnz.de](https://overflow.smnz.de) | Germany | Operated by [smnz.de](https://smnz.de) |
| Instance URL | Region | Notes |
| ------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
| [code.whatever.social](https://code.whatever.social) | United States & The Netherlands | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
| [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) |
| [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/) |
| [overflow.hostux.net](https://overflow.hostux.net/) | France | Operated by [Hostux](https://hostux.net/) |
| [overflow.projectsegfau.lt](https://overflow.projectsegfau.lt/) | United States, France, India | Operated by [Project Segfault](https://projectsegfau.lt/) |
| [code.xbdm.fun](https://code.xbdm.fun) | Germany | Operated by [xbdm.fun](https://xbdm.fun) |
| [overflow.fascinated.cc](https://overflow.fascinated.cc/) | Germany | Operated by [fascinated.cc](https://fascinated.cc/) |
| [ao.bloatcat.tk](https://ao.bloatcat.tk) | Iceland | Operated by [bloatcat.tk](https://bloatcat.tk) |
| [anonoverflow.frontendfriendly.xyz](https://anonoverflow.frontendfriendly.xyz/) | United States | Operated by [frontendfriendly.xyz](https://frontendfriendly.xyz/) |
| [ao.owo.si](https://ao.owo.si/) | Germany | Operated by [owo.si](https://owo.si/) |
| [overflow.datura.network](https://overflow.datura.network/) | Germany | Operated by [datura.network](https://datura.network) |
| | | |
## 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) |
| [overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion](http://overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/) | Luxembourg | Operated by [Project Segfault](https://projectsegfau.lt/) |
| [overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion](http://overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/) | Germany | Operated by [datura.network](https://datura.network) |
| [ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion](http://ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion/) | Germany | Operated by [owo.si](https://owo.si/) |
| [ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p](http://ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p/) | Germany | Operated by [owo.si](https://owo.si/) |
## 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
@@ -38,7 +52,7 @@ 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.
@@ -47,10 +61,16 @@ StackOverflow does not sell your information, but it does share it with third-pa
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.
## 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.
@@ -73,9 +93,9 @@ You can easily convert StackOverflow URLs to AnonymousOverflow ones by adding th
```js
javascript: (function () {
window.location = window.location
.toString()
.replace(/stackoverflow\.com/, "code.whatever.social");
window.location = window.location
.toString()
.replace(/stackoverflow\.com/, "code.whatever.social");
})();
```
@@ -91,8 +111,8 @@ Read the [wiki page](https://github.com/httpjamesm/AnonymousOverflow/wiki/Deploy
## Attribution
- Icons provided by [heroicons](https://heroicons.com) under the [MIT License](https://choosealicense.com/licenses/mit/)
- [Gin](https://github.com/gin-gonic/gin) under the [MIT License](https://github.com/gin-gonic/gin/blob/master/LICENSE)
- [goquery](https://github.com/PuerkitoBio/goquery) under the [BSD 3-Clause License](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
- [resty](https://github.com/go-resty/resty) under the [MIT License](https://github.com/go-resty/resty/blob/master/LICENSE)
- [Chroma](https://github.com/alecthomas/chroma) under the [MIT License](https://github.com/alecthomas/chroma/blob/master/COPYING)
- Icons provided by [heroicons](https://heroicons.com) under the [MIT License](https://choosealicense.com/licenses/mit/)
- [Gin](https://github.com/gin-gonic/gin) under the [MIT License](https://github.com/gin-gonic/gin/blob/master/LICENSE)
- [goquery](https://github.com/PuerkitoBio/goquery) under the [BSD 3-Clause License](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
- [resty](https://github.com/go-resty/resty) under the [MIT License](https://github.com/go-resty/resty/blob/master/LICENSE)
- [Chroma](https://github.com/alecthomas/chroma) under the [MIT License](https://github.com/alecthomas/chroma/blob/master/COPYING)
+1 -1
View File
@@ -1,3 +1,3 @@
package config
var Version = "1.4"
var Version = "1.8.3"
+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) {
+5 -4
View File
@@ -7,6 +7,8 @@ require (
github.com/alecthomas/chroma v0.10.0
github.com/gin-gonic/gin v1.8.2
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-jwt/jwt/v4 v4.4.3
github.com/joho/godotenv v1.4.0
)
require (
@@ -17,7 +19,6 @@ 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/joho/godotenv v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
@@ -26,9 +27,9 @@ require (
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/ugorji/go/codec v1.2.8 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+8 -6
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=
@@ -78,20 +80,20 @@ golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+21 -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,28 @@ 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)
r.Run(fmt.Sprintf("%s:%s", host, port))
}
+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

+85 -8
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,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;
+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))))
}
+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())
}
+67 -31
View File
@@ -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()
@@ -93,7 +122,7 @@ func ViewQuestion(c *gin.Context) {
return
}
newFilteredQuestion.Body = template.HTML(questionBodyParentHTML)
newFilteredQuestion.Body = template.HTML(utils.ReplaceImgTags(questionBodyParentHTML))
questionBodyText := questionBodyParent.Text()
@@ -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,10 +265,10 @@ 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(answerBodyHTML)
newFilteredAnswer.Body = template.HTML(utils.ReplaceImgTags(answerBodyHTML))
answers = append(answers, newFilteredAnswer)
})
@@ -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,
})
}
+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
+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
}
+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()
+61
View File
@@ -0,0 +1,61 @@
package utils
import (
"anonymousoverflow/src/types"
"fmt"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v4"
)
var imgTagRegex = regexp.MustCompile(`<img[^>]*\s+src\s*=\s*"(.*?)"[^>]*>`)
func ReplaceImgTags(inHtml string) string {
imgTags := imgTagRegex.FindAllString(inHtml, -1)
var wg sync.WaitGroup
var mu sync.Mutex
for _, imgTag := range imgTags {
wg.Add(1)
go func(imgTag string) {
defer wg.Done()
srcRegex := regexp.MustCompile(`src\s*=\s*"(.*?)"`)
src := srcRegex.FindStringSubmatch(imgTag)[1]
authToken, _ := generateImageProxyAuth(src)
mu.Lock()
defer mu.Unlock()
inHtml = strings.Replace(inHtml, imgTag, fmt.Sprintf(`<img src="%s/proxy?auth=%s">`, os.Getenv("APP_URL"), authToken), 1)
}(imgTag)
}
wg.Wait()
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
}
+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
+40 -13
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html data-theme="{{ .theme }}">
<head>
<title>{{ .question.Title }}</title>
<title>{{ .question.Title }} | AnonymousOverflow</title>
<link rel="stylesheet" href="/static/question.css" />
<link rel="stylesheet" href="/static/syntax.css" />
<link rel="stylesheet" href="/static/comments.css" />
@@ -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>