Compare commits

...

86 Commits

Author SHA1 Message Date
httpjamesm 3d7129a01f feat: docker image action 2023-09-24 00:54:46 -04:00
Korbs 20dd62726c Add SudoVanilla to instance list (#58) 2023-09-23 22:39:14 -04:00
httpjamesm 4641b5f0a3 Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2023-09-19 10:30:48 -04:00
httpjamesm 6d6fd3c560 docs: add ao.ftw.lol and anonoverflow.hyperreal.coffee 2023-09-19 10:26:46 -04:00
Whatever Social ff49961c02 Add hyperreal and update Whatever region (#57) 2023-09-19 10:24:08 -04:00
httpjamesm 8d63054f17 docs: add opnxng instance 2023-08-21 02:13:09 -04:00
httpjamesm 9cc07387ae docs: add freedit instance 2023-08-21 02:12:23 -04:00
httpjamesm 262aa01568 version 1.9.0 2023-08-21 02:05:09 -04:00
Baalaji f4ecc9be6a Switch to scratch on Dockerfile (#41) 2023-08-21 02:04:45 -04:00
httpjamesm 669187dd4d refactor: concurrently proxy img tags (#53) 2023-08-21 02:02:12 -04:00
httpjamesm 9e806186b7 style: narrower questions on mobile (#54) 2023-08-21 02:02:04 -04:00
httpjamesm 2ef7e05f4b Redirect-short-links (#50)
* feat: redirect shortened URLs

* feat: accept shortened URLs in converter

* fix: tell resty not to follow redirect

* fix: remove log
2023-08-21 02:01:56 -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 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
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
30 changed files with 657 additions and 105 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
+47
View File
@@ -0,0 +1,47 @@
name: Create and publish a Docker image
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
push:
branches: ['release']
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2 -1
View File
@@ -1,4 +1,5 @@
.env
docker-compose.yml
.DS_Store
*bin
*bin
/tmp
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}
+14 -9
View File
@@ -1,19 +1,24 @@
FROM golang:1.18-alpine
RUN apk add musl-dev
RUN apk add libc-dev
RUN apk add gcc
FROM golang:1.19-alpine as build
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY ./ /app
COPY . .
RUN go build -o /anonymousoverflow
ENV CGO_ENABLED=0
RUN go build -o anonymousoverflow
FROM scratch
COPY --from=build /app/anonymousoverflow /anonymousoverflow
COPY templates /templates
COPY public /public
COPY --from=build /etc/ssl/certs /etc/ssl/certs
EXPOSE 8080
+40 -15
View File
@@ -6,27 +6,46 @@ 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) |
| [overflow.freedit.eu](overflow.freedit.eu) | United States | Operated by [freedit.eu](https://freedit.eu) |
| [ao.ftw.lol](https://ao.ftw.lol) | Germany | Operated by [ftw.lol](https://ftw.lol) |
| [anonoverflow.hyperreal.coffee](https://anonoverflow.hyperreal.coffee) | United States | Operated by [hyperreal.coffee](https://hyperreal.coffee) |
| [a.opnxng.com](a.opnxng.com) | Singapore | Operated by [opnxng.com](https://about.opnxng.com) |
| [overflow.sudovanilla.com](https://overflow.sudovanilla.com) | United States | Operated by [SudoVanilla](https://sudovanilla.com)|
## 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?
@@ -51,6 +70,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.
@@ -75,8 +100,8 @@ You can easily convert StackOverflow URLs to AnonymousOverflow ones by adding th
javascript: (function () {
window.location = window.location
.toString()
.replace(/stackoverflow\.com/, "code.whatever.social");
})();
.replace(/stackoverflow\.com/, 'code.whatever.social')
})()
```
Replace `code.whatever.social` with the domain name of the instance you're using if needed.
+1 -1
View File
@@ -1,3 +1,3 @@
package config
var Version = "1.4"
var Version = "1.9.0"
+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=
+23 -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,30 @@ func main() {
r.GET("/options/:name", routes.ChangeOptions)
r.GET("/", routes.GetHome)
r.POST("/", routes.PostHome)
r.GET("/a/:id", routes.RedirectShortenedOverflowURL)
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 -4
View File
@@ -10,7 +10,7 @@
--link-color: #92adff;
}
[data-theme="light"] {
[data-theme='light'] {
--main-bg: rgb(219, 219, 219);
--text-color: #000;
--muted-text-color: #636363;
@@ -26,7 +26,6 @@ a {
color: var(--link-color);
}
html {
margin: 0;
padding: 0;
@@ -45,9 +44,9 @@ html {
.icon img {
background: white;
border-radius: 50%;
padding: .25rem;
padding: 0.25rem;
}
details {
cursor: pointer;
}
}
+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

+96 -11
View File
@@ -19,9 +19,17 @@ body {
box-sizing: border-box;
}
.parent {
max-width: 90%;
width: fit-content;
@media (orientation: landscape) {
.parent {
max-width: 50%;
width: fit-content;
}
}
@media (orientation: portrait) {
.parent {
max-width: 90%;
width: fit-content;
}
}
.header {
@@ -29,6 +37,20 @@ body {
justify-content: space-between;
}
.card-tags {
display: flex;
gap: 0.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 +67,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 +102,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 +148,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
+20 -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{}
@@ -32,16 +35,29 @@ func PostHome(c *gin.Context) {
soLink := body.URL
// remove the www.
soLink = strings.ReplaceAll(soLink, "www.", "")
// validate URL
if !strings.HasPrefix(soLink, "https://stackoverflow.com/questions/") {
isStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/questions/")
isShortenedStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/a/")
isStackExchange := stackExchangeRegex.MatchString(soLink)
if !isStackExchange && !isStackOverflow && !isShortenedStackOverflow {
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 || isShortenedStackOverflow {
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,
})
}
+44
View File
@@ -0,0 +1,44 @@
package routes
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)
func RedirectShortenedOverflowURL(c *gin.Context) {
id := c.Param("id")
// fetch the stack overflow URL
client := resty.New()
client.SetRedirectPolicy(
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}),
)
resp, err := client.R().Get(fmt.Sprintf("https://www.stackoverflow.com/a/%s", id))
if err != nil {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Unable to fetch stack overflow URL",
"theme": c.MustGet("theme").(string),
})
return
}
if resp.StatusCode() != 302 {
c.HTML(400, "home.html", gin.H{
"errorMessage": fmt.Sprintf("Unexpected HTTP status from origin: %d", resp.StatusCode()),
"theme": c.MustGet("theme").(string),
})
return
}
// get the redirect URL
location := resp.Header().Get("Location")
c.Redirect(302, fmt.Sprintf("%s%s", os.Getenv("APP_URL"), location))
}
+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>