mirror of
https://github.com/httpjamesm/AnonymousOverflow.git
synced 2024-12-06 19:16:43 +01:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70b09e14ab | |||
| e82646635e | |||
| ff66f41f47 | |||
| 01b960cd43 | |||
| 0058aea03b | |||
| 42ad68fe34 | |||
| 634c7f1ad0 | |||
| 4db7b4795b | |||
| 13418054b4 | |||
| ea455f9317 | |||
| aa74bbc5fc | |||
| 56c9e4b196 | |||
| 16828585ac | |||
| cf89be2a48 | |||
| dcf0805334 | |||
| bb629da9a9 | |||
| d56a9309b9 | |||
| 10fc103fda | |||
| 991b5b46f4 | |||
| 7cf3839f41 | |||
| 3424b363d4 | |||
| 3d7129a01f | |||
| 20dd62726c | |||
| 4641b5f0a3 | |||
| 6d6fd3c560 | |||
| ff49961c02 | |||
| 8d63054f17 | |||
| 9cc07387ae | |||
| 262aa01568 | |||
| f4ecc9be6a | |||
| 669187dd4d | |||
| 9e806186b7 | |||
| 2ef7e05f4b | |||
| 4760681c55 | |||
| 6495e4a4bd | |||
| 9a6a044d20 | |||
| 7201269609 | |||
| a666e02256 | |||
| 2fe61471fb | |||
| 4e8a7d3553 | |||
| 2bbed5606d | |||
| dbbc9570db | |||
| 37d28dd704 | |||
| 236eb849e4 | |||
| ee5f4eb4b6 | |||
| 975ed51726 | |||
| 5b8e6acf9e | |||
| 92b8707882 | |||
| c95c2d74b3 | |||
| 296b30323a | |||
| 225420029e | |||
| 6736b6440c | |||
| 7dc9856643 | |||
| 5bc43af96d | |||
| 2afecd8ae7 | |||
| bdca47a51e | |||
| 51190f8d69 | |||
| bfa19bc2d2 | |||
| 55b06b986b | |||
| 09de9f11a5 | |||
| 63e9792d2a | |||
| 8ee1d76993 | |||
| 6b77c5dae4 | |||
| df93607e5d | |||
| 05fbd9608e | |||
| 66db070fa5 | |||
| bb9f8e2aa2 | |||
| 487a62e7aa | |||
| abfd0cfb6f | |||
| 63a30ab1cf | |||
| f1370c29b6 | |||
| d935ce72f6 | |||
| 45644a0503 | |||
| f81b726612 | |||
| c55b8b6ed5 | |||
| ba55578143 | |||
| 95e270d9f3 | |||
| 0968932eff | |||
| 64302c9334 | |||
| 9d28ee483c | |||
| 4fa89879cf | |||
| 203e8aa23c | |||
| cc9e549aaf | |||
| fae2717a35 | |||
| cf8892b4cb | |||
| c8a394e0ce | |||
| 6bfed5a9e3 | |||
| a0df61a92d | |||
| bbf098e7f4 | |||
| 2c4ded0800 | |||
| f0e42287cf | |||
| 9ebb6c90d9 | |||
| 69d96d646d | |||
| c87ce26da8 | |||
| 3dea27b09f | |||
| 4d8632e26c | |||
| eee8ec42c5 | |||
| d7bca93af8 | |||
| 2b608b4725 | |||
| bd48b309ac | |||
| 15b843d46b | |||
| 7b80c55dd2 | |||
| 12174b2600 | |||
| 5f11bcd6a2 | |||
| 7494b2df33 | |||
| 517a9afb89 | |||
| f9e42ca8c7 | |||
| 3aac1467cd | |||
| 062ffbcbdc | |||
| aa13c8515f | |||
| d49c0f2ffd | |||
| 89d8dee958 | |||
| 0ad3f08ef5 | |||
| d5bcf7fa38 | |||
| 95df8a715b | |||
| f2b369b2c6 | |||
| 17a8c6b1e1 | |||
| be9380ba20 | |||
| e4d429b642 | |||
| b35c75cdf1 | |||
| c46820b6d4 | |||
| 0e9677ec61 | |||
| 4ddc1a2af7 | |||
| e476087a3a | |||
| 3ff8e8cb96 | |||
| 1d12372ea5 | |||
| b038d2c502 | |||
| e6915ae849 | |||
| 572fac9ec3 | |||
| 11efaf854d | |||
| 3b79efc83c | |||
| c0df07abad | |||
| 10e7c89e96 | |||
| 6f86cbce6e | |||
| 1d2e98e4c1 | |||
| 599d23544b | |||
| 84ba0886cd | |||
| 5bd9ce484f | |||
| 8f9091fb2a | |||
| 1b1120d924 | |||
| a327366400 | |||
| 97e2e50015 | |||
| 96d4f768d9 | |||
| 14b11b7f14 | |||
| 631a700c9f | |||
| da57f98fec | |||
| b68786a50f | |||
| 819a8fe39f | |||
| 0d5f0a04f1 | |||
| 46545385e1 | |||
| ef723a0c21 | |||
| 0e2e8b26f6 | |||
| 5a1e1ca652 | |||
| 5373b9d979 | |||
| a3b69f37a2 | |||
| f9025ff9cc |
@@ -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 = true
|
||||
|
||||
[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
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 'Create a report to help us improve'
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please make sure you're on the latest version before submitting.
|
||||
|
||||
|
||||
# What's Happening?
|
||||
<!-- Describe here -->
|
||||
|
||||
|
||||
## How to reproduce:
|
||||
<!-- Describe here -->
|
||||
|
||||
|
||||
## Affected Platforms:
|
||||
|
||||
- [ ] macOS
|
||||
- [ ] Windows
|
||||
- [ ] Linux (Specify)
|
||||
- [ ] iOS
|
||||
- [ ] Android
|
||||
|
||||
Version:
|
||||
|
||||
## Browser:
|
||||
|
||||
- [ ] Chromium-based (ex: Brave or Chrome)
|
||||
- [ ] Webkit-based (ex: Safari)
|
||||
- [ ] Gecko-based (ex: Firefox)
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: New Instance
|
||||
about: 'Add my public instance to the list'
|
||||
title: "[INSTANCE] New public instance"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Instance URL:
|
||||
Region (Written Out - ex United States):
|
||||
Operated by (Link to your site):
|
||||
@@ -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 }}
|
||||
+4
-1
@@ -1,2 +1,5 @@
|
||||
.env
|
||||
docker-compose.yml
|
||||
docker-compose.yml
|
||||
.DS_Store
|
||||
*bin
|
||||
/tmp
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
+14
-9
@@ -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
|
||||
|
||||
|
||||
@@ -6,17 +6,79 @@ This project is super lightweight by design. The UI is simple and the frontend i
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Instances
|
||||
## 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) |
|
||||
| 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) |
|
||||
| [anonymousoverflow.privacyfucking.rocks](https://anonymousoverflow.privacyfucking.rocks/) | Germany | Operated by [privacyfucking.rocks](https://privacyfucking.rocks) |
|
||||
| [exchange.seitan-ayoub.lol](https://exchange.seitan-ayoub.lol) | Germany | Operated by [Seitan Ayoub](https://seitan-ayoub.lol) |
|
||||
| [overflow.r4fo.com](https://overflow.r4fo.com) | The Netherlands | Operated by [r4fo.com](https://r4fo.com) |
|
||||
| [overflow.ducks.party](https://overflow.ducks.party) | The Netherlands | Operated by [ducks.party](https://ducks.party) |
|
||||
| [ao.ngn.tf](https://ao.ngn.tf) | Turkey | Operated by [ngn](https://ngn.tf) |
|
||||
|
||||
## 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) |
|
||||
| [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
|
||||
|
||||
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
|
||||
|
||||
> internet protocol (IP) address, [...] browser type and version, time zone setting and location, browser plug-in types and versions, operating system, and platform [...] data
|
||||
|
||||
to be collected and stored.
|
||||
|
||||
- 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.
|
||||
|
||||
Their main website also [contains trackers from Alphabet](https://themarkup.org/blacklight?url=stackoverflow.com).
|
||||
|
||||
- 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
|
||||
|
||||
@@ -34,16 +96,33 @@ becomes
|
||||
${instanceURL}/questions/43743250/using-libsodium-xchacha20-poly1305-for-large-files
|
||||
```
|
||||
|
||||
### Bookmark Conversion Tool
|
||||
|
||||
You can easily convert StackOverflow URLs to AnonymousOverflow ones by adding the following code as a bookmark in your web browser:
|
||||
|
||||
```js
|
||||
javascript: (function () {
|
||||
window.location = window.location
|
||||
.toString()
|
||||
.replace(/stackoverflow\.com/, 'code.whatever.social')
|
||||
})()
|
||||
```
|
||||
|
||||
Replace `code.whatever.social` with the domain name of the instance you're using if needed.
|
||||
|
||||
You can run this bookmarklet on any StackOverflow page to view it anonymously.
|
||||
|
||||
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
|
||||
|
||||
- Icons provided by [heroicons](https://heroicons.com) under the [MIT License](https://choosealicense.com/licenses/mit/)
|
||||
- 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)
|
||||
- [KaTeX](https://github.com/KaTeX/KaTeX) under the [MIT License](https://github.com/KaTeX/KaTeX/blob/main/LICENSE)
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package config
|
||||
|
||||
var Version = "1.1"
|
||||
var Version = "1.10.1"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
anonymousoverflow:
|
||||
container_name: 'app'
|
||||
build:
|
||||
context: .
|
||||
network: 'host'
|
||||
environment:
|
||||
- APP_URL=https://domain.com
|
||||
ports:
|
||||
- '80:8080'
|
||||
restart: 'always'
|
||||
anonymousoverflow:
|
||||
container_name: 'app'
|
||||
image: 'ghcr.io/httpjamesm/anonymousoverflow:release'
|
||||
environment:
|
||||
- APP_URL=https://domain.com
|
||||
- JWT_SIGNING_SECRET=secret
|
||||
ports:
|
||||
- '80:8080'
|
||||
restart: 'always'
|
||||
|
||||
Vendored
+4
@@ -3,10 +3,14 @@ package env
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func RunChecks() {
|
||||
godotenv.Load(".env")
|
||||
checkEnv("APP_URL")
|
||||
checkEnv("JWT_SIGNING_SECRET")
|
||||
}
|
||||
|
||||
func checkEnv(key string) {
|
||||
|
||||
@@ -3,14 +3,22 @@ module anonymousoverflow
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
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 (
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.8.2 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
@@ -18,11 +26,14 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // 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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
||||
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
@@ -20,16 +27,23 @@ 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=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
@@ -43,8 +57,10 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@@ -54,41 +70,46 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0=
|
||||
github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
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 h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
|
||||
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=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -32,12 +32,15 @@ func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.LoadHTMLGlob("templates/*")
|
||||
r.Static("/static", "./public")
|
||||
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middleware.XssPreventionHeaders())
|
||||
r.Use(middleware.NoCacheMiddleware())
|
||||
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: /")
|
||||
})
|
||||
@@ -45,10 +48,32 @@ func main() {
|
||||
r.GET("/options/:name", routes.ChangeOptions)
|
||||
|
||||
r.GET("/", routes.GetHome)
|
||||
|
||||
r.POST("/", routes.PostHome)
|
||||
|
||||
r.GET("/a/:id", routes.RedirectShortenedOverflowURL)
|
||||
r.GET("/q/:id", routes.RedirectShortenedOverflowURL)
|
||||
r.GET("/q/:id/:answerId", 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))
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
@@ -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;
|
||||
}
|
||||
+19
-4
@@ -10,11 +10,11 @@
|
||||
--link-color: #92adff;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
[data-theme='light'] {
|
||||
--main-bg: rgb(219, 219, 219);
|
||||
--text-color: #000;
|
||||
--muted-text-color: #636363;
|
||||
--code-bg: #b5b5b5;
|
||||
--code-bg: #36383d;
|
||||
--input-bg: rgb(188, 188, 188);
|
||||
--input-bg-hover: rgb(168, 168, 168);
|
||||
--meta-bg: rgb(170, 168, 168);
|
||||
@@ -26,7 +26,6 @@ a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -45,5 +44,21 @@ html {
|
||||
.icon img {
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
padding: .25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fd-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fw-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
+20
-10
@@ -25,13 +25,13 @@ body {
|
||||
|
||||
.view-form {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-input {
|
||||
width: 100%;
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -48,7 +48,7 @@ body {
|
||||
}
|
||||
|
||||
.view-button {
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -65,29 +65,39 @@ body {
|
||||
background-color: var(--input-bg-hover);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: .5rem;
|
||||
background-color: rgb(255, 129, 129);
|
||||
.success {
|
||||
background-color: #8cffc0;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgb(255, 129, 129);
|
||||
}
|
||||
|
||||
.error,
|
||||
.success {
|
||||
color: black;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error p {
|
||||
.error p,
|
||||
.success p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -100,4 +110,4 @@ body {
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -0,0 +1,125 @@
|
||||
<h1><a href="https://katex.org/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://katex.org/img/katex-logo.svg">
|
||||
<img alt="KaTeX" width=130 src="https://katex.org/img/katex-logo-black.svg">
|
||||
</picture>
|
||||
</a></h1>
|
||||
|
||||
[](https://www.npmjs.com/package/katex)
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||
[](https://github.com/KaTeX/KaTeX/actions?query=workflow%3ACI)
|
||||
[](https://codecov.io/gh/KaTeX/KaTeX)
|
||||
[](https://github.com/KaTeX/KaTeX/discussions)
|
||||
[](https://www.jsdelivr.com/package/npm/katex)
|
||||

|
||||
[](https://gitpod.io/#https://github.com/KaTeX/KaTeX)
|
||||
[](https://opencollective.com/katex)
|
||||
|
||||
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
|
||||
|
||||
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](https://www.intmath.com/cg5/katex-mathjax-comparison.php).
|
||||
* **Print quality:** KaTeX's layout is based on Donald Knuth's TeX, the gold standard for math typesetting.
|
||||
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
|
||||
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
|
||||
|
||||
KaTeX is compatible with all major browsers, including Chrome, Safari, Firefox, Opera, Edge, and IE 11.
|
||||
|
||||
KaTeX supports much (but not all) of LaTeX and many LaTeX packages. See the [list of supported functions](https://katex.org/docs/supported.html).
|
||||
|
||||
Try out KaTeX [on the demo page](https://katex.org/#demo)!
|
||||
|
||||
## Getting started
|
||||
|
||||
### Starter template
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!-- KaTeX requires the use of the HTML5 doctype. Without it, KaTeX may not render properly -->
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css" integrity="sha384-GvrOXuhMATgEsSwCs4smul74iXGOixntILdUW9XmUC6+HX0sLNAK3q71HotJqlAn" crossorigin="anonymous">
|
||||
|
||||
<!-- The loading of KaTeX is deferred to speed up page rendering -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js" integrity="sha384-cpW21h6RZv/phavutF+AuVYrr+dA8xD9zs6FwLpaCct6O9ctzYFfFr4dgmgccOTx" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- To automatically render math in text elements, include the auto-render extension: -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous"
|
||||
onload="renderMathInElement(document.body);"></script>
|
||||
</head>
|
||||
...
|
||||
</html>
|
||||
```
|
||||
|
||||
You can also [download KaTeX](https://github.com/KaTeX/KaTeX/releases) and host it yourself.
|
||||
|
||||
For details on how to configure auto-render extension, refer to [the documentation](https://katex.org/docs/autorender.html).
|
||||
|
||||
### API
|
||||
|
||||
Call `katex.render` to render a TeX expression directly into a DOM element.
|
||||
For example:
|
||||
|
||||
```js
|
||||
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, {
|
||||
throwOnError: false
|
||||
});
|
||||
```
|
||||
|
||||
Call `katex.renderToString` to generate an HTML string of the rendered math,
|
||||
e.g., for server-side rendering. For example:
|
||||
|
||||
```js
|
||||
var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}", {
|
||||
throwOnError: false
|
||||
});
|
||||
// '<span class="katex">...</span>'
|
||||
```
|
||||
|
||||
Make sure to include the CSS and font files in both cases.
|
||||
If you are doing all rendering on the server, there is no need to include the
|
||||
JavaScript on the client.
|
||||
|
||||
The examples above use the `throwOnError: false` option, which renders invalid
|
||||
inputs as the TeX source code in red (by default), with the error message as
|
||||
hover text. For other available options, see the
|
||||
[API documentation](https://katex.org/docs/api.html),
|
||||
[options documentation](https://katex.org/docs/options.html), and
|
||||
[handling errors documentation](https://katex.org/docs/error.html).
|
||||
|
||||
## Demo and Documentation
|
||||
|
||||
Learn more about using KaTeX [on the website](https://katex.org)!
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute code. If you'd like to help, see [our guide to contributing code](CONTRIBUTING.md).
|
||||
<a href="https://github.com/KaTeX/KaTeX/graphs/contributors"><img src="https://contributors-svg.opencollective.com/katex/contributors.svg?width=890&button=false" alt="Code contributors" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community.
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/katex"><img src="https://opencollective.com/katex/individuals.svg?width=890" alt="Contribute on Open Collective"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website.
|
||||
|
||||
<a href="https://opencollective.com/katex/organization/0/website"><img src="https://opencollective.com/katex/organization/0/avatar.svg" alt="Organization 1"></a>
|
||||
<a href="https://opencollective.com/katex/organization/1/website"><img src="https://opencollective.com/katex/organization/1/avatar.svg" alt="Organization 2"></a>
|
||||
<a href="https://opencollective.com/katex/organization/2/website"><img src="https://opencollective.com/katex/organization/2/avatar.svg" alt="Organization 3"></a>
|
||||
<a href="https://opencollective.com/katex/organization/3/website"><img src="https://opencollective.com/katex/organization/3/avatar.svg" alt="Organization 4"></a>
|
||||
<a href="https://opencollective.com/katex/organization/4/website"><img src="https://opencollective.com/katex/organization/4/avatar.svg" alt="Organization 5"></a>
|
||||
<a href="https://opencollective.com/katex/organization/5/website"><img src="https://opencollective.com/katex/organization/5/avatar.svg" alt="Organization 6"></a>
|
||||
<a href="https://opencollective.com/katex/organization/6/website"><img src="https://opencollective.com/katex/organization/6/avatar.svg" alt="Organization 7"></a>
|
||||
<a href="https://opencollective.com/katex/organization/7/website"><img src="https://opencollective.com/katex/organization/7/avatar.svg" alt="Organization 8"></a>
|
||||
<a href="https://opencollective.com/katex/organization/8/website"><img src="https://opencollective.com/katex/organization/8/avatar.svg" alt="Organization 9"></a>
|
||||
<a href="https://opencollective.com/katex/organization/9/website"><img src="https://opencollective.com/katex/organization/9/avatar.svg" alt="Organization 10"></a>
|
||||
|
||||
## License
|
||||
|
||||
KaTeX is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var i=r[e];if(void 0!==i)return i.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var i={};return function(){n.d(i,{default:function(){return s}});var e=n(771),t=n.n(e),r=function(e,t,r){for(var n=r,i=0,a=e.length;n<t.length;){var o=t[n];if(i<=0&&t.slice(n,n+a)===e)return n;"\\"===o?n++:"{"===o?i++:"}"===o&&i--,n++}return-1},a=/^\\begin{/,o=function(e,t){for(var n,i=[],o=new RegExp("("+t.map((function(e){return e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")})).join("|")+")");-1!==(n=e.search(o));){n>0&&(i.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));var l=t.findIndex((function(t){return e.startsWith(t.left)}));if(-1===(n=r(t[l].right,e,t[l].left.length)))break;var d=e.slice(0,n+t[l].right.length),s=a.test(d)?d:e.slice(t[l].left.length,n);i.push({type:"math",data:s,rawData:d,display:t[l].display}),e=e.slice(n+t[l].right.length)}return""!==e&&i.push({type:"text",data:e}),i},l=function(e,r){var n=o(e,r.delimiters);if(1===n.length&&"text"===n[0].type)return null;for(var i=document.createDocumentFragment(),a=0;a<n.length;a++)if("text"===n[a].type)i.appendChild(document.createTextNode(n[a].data));else{var l=document.createElement("span"),d=n[a].data;r.displayMode=n[a].display;try{r.preProcess&&(d=r.preProcess(d)),t().render(d,l,r)}catch(e){if(!(e instanceof t().ParseError))throw e;r.errorCallback("KaTeX auto-render: Failed to parse `"+n[a].data+"` with ",e),i.appendChild(document.createTextNode(n[a].rawData));continue}i.appendChild(l)}return i},d=function e(t,r){for(var n=0;n<t.childNodes.length;n++){var i=t.childNodes[n];if(3===i.nodeType){for(var a=i.textContent,o=i.nextSibling,d=0;o&&o.nodeType===Node.TEXT_NODE;)a+=o.textContent,o=o.nextSibling,d++;var s=l(a,r);if(s){for(var f=0;f<d;f++)i.nextSibling.remove();n+=s.childNodes.length-1,t.replaceChild(s,i)}else n+=d}else 1===i.nodeType&&function(){var t=" "+i.className+" ";-1===r.ignoredTags.indexOf(i.nodeName.toLowerCase())&&r.ignoredClasses.every((function(e){return-1===t.indexOf(" "+e+" ")}))&&e(i,r)}()}},s=function(e,t){if(!e)throw new Error("No element provided to render");var r={};for(var n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);r.delimiters=r.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],r.ignoredTags=r.ignoredTags||["script","noscript","style","textarea","pre","code","option"],r.ignoredClasses=r.ignoredClasses||[],r.errorCallback=r.errorCallback||console.error,r.macros=r.macros||{},d(e,r)}}(),i=i.default}()}));
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+109
-13
@@ -8,7 +8,7 @@ body {
|
||||
background-color: var(--main-bg);
|
||||
color: var(--text-color);
|
||||
font-family: sans-serif;
|
||||
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -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,11 +37,25 @@ 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: .15rem;
|
||||
padding: 0.15rem;
|
||||
border-radius: 5px;
|
||||
color: var(--text-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
pre {
|
||||
@@ -45,24 +67,34 @@ pre {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.answer-divider {
|
||||
.post-divider {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
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;
|
||||
@@ -70,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 {
|
||||
@@ -85,10 +125,10 @@ img {
|
||||
.answer-author {
|
||||
width: fit-content;
|
||||
background-color: var(--meta-bg);
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
text-align: right;
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
@@ -100,9 +140,65 @@ img {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.s-notice {
|
||||
background-color: var(--meta-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
color: var(--text-color);
|
||||
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;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const doRender = () => {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
doRender();
|
||||
@@ -0,0 +1,60 @@
|
||||
/* Background */ .bg { color: #f8f8f2; background-color: #272822; }
|
||||
/* Error */ .err { color: #960050; background-color: #1e0010 }
|
||||
/* LineTableTD */ .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||
/* LineTable */ .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||
/* LineHighlight */ .hl { background-color: #3c3d38 }
|
||||
/* LineNumbersTable */ .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* LineNumbers */ .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* Line */ .line { display: flex; }
|
||||
/* Keyword */ .k { color: #66d9ef }
|
||||
/* KeywordConstant */ .kc { color: #66d9ef }
|
||||
/* KeywordDeclaration */ .kd { color: #66d9ef }
|
||||
/* KeywordNamespace */ .kn { color: #f92672 }
|
||||
/* KeywordPseudo */ .kp { color: #66d9ef }
|
||||
/* KeywordReserved */ .kr { color: #66d9ef }
|
||||
/* KeywordType */ .kt { color: #66d9ef }
|
||||
/* NameAttribute */ .na { color: #a6e22e }
|
||||
/* NameClass */ .nc { color: #a6e22e }
|
||||
/* NameConstant */ .no { color: #66d9ef }
|
||||
/* NameDecorator */ .nd { color: #a6e22e }
|
||||
/* NameException */ .ne { color: #a6e22e }
|
||||
/* NameFunction */ .nf { color: #a6e22e }
|
||||
/* NameOther */ .nx { color: #a6e22e }
|
||||
/* NameTag */ .nt { color: #f92672 }
|
||||
/* Literal */ .l { color: #ae81ff }
|
||||
/* LiteralDate */ .ld { color: #e6db74 }
|
||||
/* LiteralString */ .s { color: #e6db74 }
|
||||
/* LiteralStringAffix */ .sa { color: #e6db74 }
|
||||
/* LiteralStringBacktick */ .sb { color: #e6db74 }
|
||||
/* LiteralStringChar */ .sc { color: #e6db74 }
|
||||
/* LiteralStringDelimiter */ .dl { color: #e6db74 }
|
||||
/* LiteralStringDoc */ .sd { color: #e6db74 }
|
||||
/* LiteralStringDouble */ .s2 { color: #e6db74 }
|
||||
/* LiteralStringEscape */ .se { color: #ae81ff }
|
||||
/* LiteralStringHeredoc */ .sh { color: #e6db74 }
|
||||
/* LiteralStringInterpol */ .si { color: #e6db74 }
|
||||
/* LiteralStringOther */ .sx { color: #e6db74 }
|
||||
/* LiteralStringRegex */ .sr { color: #e6db74 }
|
||||
/* LiteralStringSingle */ .s1 { color: #e6db74 }
|
||||
/* LiteralStringSymbol */ .ss { color: #e6db74 }
|
||||
/* LiteralNumber */ .m { color: #ae81ff }
|
||||
/* LiteralNumberBin */ .mb { color: #ae81ff }
|
||||
/* LiteralNumberFloat */ .mf { color: #ae81ff }
|
||||
/* LiteralNumberHex */ .mh { color: #ae81ff }
|
||||
/* LiteralNumberInteger */ .mi { color: #ae81ff }
|
||||
/* LiteralNumberIntegerLong */ .il { color: #ae81ff }
|
||||
/* LiteralNumberOct */ .mo { color: #ae81ff }
|
||||
/* Operator */ .o { color: #f92672 }
|
||||
/* OperatorWord */ .ow { color: #f92672 }
|
||||
/* Comment */ .c { color: #75715e }
|
||||
/* CommentHashbang */ .ch { color: #75715e }
|
||||
/* CommentMultiline */ .cm { color: #75715e }
|
||||
/* CommentSingle */ .c1 { color: #75715e }
|
||||
/* CommentSpecial */ .cs { color: #75715e }
|
||||
/* CommentPreproc */ .cp { color: #75715e }
|
||||
/* CommentPreprocFile */ .cpf { color: #75715e }
|
||||
/* GenericDeleted */ .gd { color: #f92672 }
|
||||
/* GenericEmph */ .ge { font-style: italic }
|
||||
/* GenericInserted */ .gi { color: #a6e22e }
|
||||
/* GenericStrong */ .gs { font-weight: bold }
|
||||
/* GenericSubheading */ .gu { color: #75715e }
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func NoCacheMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
+11
-15
@@ -1,6 +1,8 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func OptionsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -8,23 +10,17 @@ func OptionsMiddleware() gin.HandlerFunc {
|
||||
c.Set("theme", "dark")
|
||||
|
||||
imagesCookie, err := c.Cookie("disable_images")
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if imagesCookie == "true" {
|
||||
c.Set("disable_images", true)
|
||||
if err == nil {
|
||||
if imagesCookie == "true" {
|
||||
c.Set("disable_images", true)
|
||||
}
|
||||
}
|
||||
|
||||
themeCookie, err := c.Cookie("theme")
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if themeCookie == "light" {
|
||||
c.Set("theme", "light")
|
||||
if err == nil {
|
||||
if themeCookie == "light" {
|
||||
c.Set("theme", "light")
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"anonymousoverflow/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -11,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
|
||||
@@ -31,14 +45,26 @@ func Ratelimit() gin.HandlerFunc {
|
||||
|
||||
// if they exceed 30 requests in 1 minute, return a 429
|
||||
if val.(int) > 30 {
|
||||
c.String(429, "429 Too Many Requests")
|
||||
c.HTML(429, "home.html", gin.H{
|
||||
"errorMessage": "You have exceeded the request limit. Please try again in a minute.",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// delete the ip from the map after 1 minute
|
||||
// subtract 1 from the value after 1 minute if the value exists and is greater than 0
|
||||
time.AfterFunc(time.Minute, func() {
|
||||
ipMap.Delete(ip)
|
||||
val, ok := ipMap.Load(ip)
|
||||
if ok && val.(int) > 0 {
|
||||
ipMap.Store(ip, val.(int)-1)
|
||||
}
|
||||
|
||||
// if the value is 0, delete the entry from the map
|
||||
if val.(int) == 0 {
|
||||
ipMap.Delete(ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func XssPreventionHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
c.Header("X-XSS-Protection", "1; mode=block")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -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/") || strings.HasPrefix(soLink, "https://stackoverflow.com/q/")
|
||||
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))))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"anonymousoverflow/config"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -18,27 +19,29 @@ func ChangeOptions(c *gin.Context) {
|
||||
text = "enabled"
|
||||
}
|
||||
c.SetCookie("disable_images", fmt.Sprintf("%t", !c.MustGet("disable_images").(bool)), 60*60*24*365*10, "/", "", false, true)
|
||||
c.String(200, "Images are now %s", text)
|
||||
c.HTML(200, "home.html", gin.H{
|
||||
"successMessage": "Images are now " + text,
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
|
||||
case "theme":
|
||||
text := "dark"
|
||||
if c.MustGet("theme").(string) == "dark" {
|
||||
text = "light"
|
||||
}
|
||||
|
||||
c.SetCookie("theme", text, 60*60*24*365*10, "/", "", false, true)
|
||||
// get redirect url from query
|
||||
redirectUrl := c.Query("redirect_url")
|
||||
if redirectUrl == "" {
|
||||
redirectUrl = os.Getenv("APP_URL")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(redirectUrl, os.Getenv("APP_URL")) {
|
||||
redirectUrl = os.Getenv("APP_URL")
|
||||
}
|
||||
|
||||
c.Redirect(302, redirectUrl)
|
||||
|
||||
default:
|
||||
c.String(400, "400 Bad Request")
|
||||
}
|
||||
|
||||
c.Redirect(302, "/")
|
||||
}
|
||||
|
||||
+232
-124
@@ -1,27 +1,67 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"anonymousoverflow/config"
|
||||
"anonymousoverflow/src/utils"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"anonymousoverflow/src/types"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
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")
|
||||
questionTitle := c.Param("title")
|
||||
if _, err := strconv.Atoi(questionId); err != nil {
|
||||
c.HTML(400, "home.html", gin.H{
|
||||
"errorMessage": "Invalid question ID",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
soLink := fmt.Sprintf("https://stackoverflow.com/questions/%s/%s", questionId, questionTitle)
|
||||
|
||||
resp, err := client.R().Get(soLink)
|
||||
params, err := parseAndValidateParameters(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return
|
||||
}
|
||||
|
||||
domain := "stackoverflow.com"
|
||||
|
||||
if params.Sub != "" {
|
||||
domain = fmt.Sprintf("%s.stackexchange.com", params.Sub)
|
||||
}
|
||||
|
||||
soLink := fmt.Sprintf("https://%s/questions/%s/%s?answertab=%s", domain, questionId, params.QuestionTitle, params.SoSortValue)
|
||||
|
||||
resp, err := fetchQuestionData(soLink)
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
c.HTML(500, "home.html", gin.H{
|
||||
"errorMessage": fmt.Sprintf("Received a non-OK status code %d", resp.StatusCode()),
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
respBody := resp.String()
|
||||
@@ -30,117 +70,33 @@ func ViewQuestion(c *gin.Context) {
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(respBodyReader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
questionTextParent := doc.Find("h1.fs-headline1")
|
||||
|
||||
questionText := questionTextParent.Children().First().Text()
|
||||
|
||||
questionBodyParent := doc.Find("div.s-prose")
|
||||
|
||||
questionBodyParentHTML, err := questionBodyParent.Html()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
questionCard := doc.Find("div.postcell")
|
||||
|
||||
questionMetadata := questionCard.Find("div.user-info")
|
||||
questionTimestamp := ""
|
||||
questionMetadata.Find("span.relativetime").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
|
||||
questionTimestamp = s.Text()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise it's the second element
|
||||
if i == 1 {
|
||||
questionTimestamp = s.Text()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
userDetails := questionMetadata.Find("div.user-details")
|
||||
|
||||
questionAuthor := ""
|
||||
questionAuthorURL := ""
|
||||
|
||||
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
|
||||
questionAuthor = s.Text()
|
||||
questionAuthorURL, _ = s.Attr("href")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise it's the second element
|
||||
if i == 1 {
|
||||
questionAuthor = s.Text()
|
||||
questionAuthorURL, _ = s.Attr("href")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
answers := []template.HTML{}
|
||||
|
||||
doc.Find("div.answer").Each(func(i int, s *goquery.Selection) {
|
||||
postLayout := s.Find("div.post-layout")
|
||||
voteCell := postLayout.Find("div.votecell")
|
||||
answerCell := postLayout.Find("div.answercell")
|
||||
answerBody := answerCell.Find("div.s-prose")
|
||||
answerBodyHTML, _ := answerBody.Html()
|
||||
|
||||
voteCount := voteCell.Find("div.js-vote-count").Text()
|
||||
|
||||
if s.HasClass("accepted-answer") {
|
||||
// add <div class="answer-meta accepted">Accepted Answer</div> to the top of the answer
|
||||
answerBodyHTML = fmt.Sprintf(`<div class="answer-meta accepted">Accepted Answer - %s Upvotes</div>`, voteCount) + answerBodyHTML
|
||||
} else {
|
||||
// add <div class="answer-meta">%s Upvotes</div> to the top of the answer
|
||||
answerBodyHTML = fmt.Sprintf(`<div class="answer-meta">%s Upvotes</div>`, voteCount) + answerBodyHTML
|
||||
}
|
||||
|
||||
answerFooter := s.Find("div.mt24")
|
||||
|
||||
answerAuthorURL := ""
|
||||
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() > 1 {
|
||||
if i == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
answerAuthor := answerAuthorDetails.Find("a").First()
|
||||
|
||||
answerAuthorURL = answerAuthor.AttrOr("href", "")
|
||||
answerAuthorName = answerAuthor.Text()
|
||||
answerTimestamp = s.Find("span.relativetime").Text()
|
||||
c.HTML(500, "home.html", gin.H{
|
||||
"errorMessage": "Unable to parse question data",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// append <div class="answer-author">Answered %s by %s</div> to the bottom of the answer
|
||||
answerBodyHTML += fmt.Sprintf(`<div class="answer-author-parent"><div class="answer-author">Answered at %s by <a href="https://stackoverflow.com/%s" target="_blank" rel="noopener noreferrer">%s</a></div></div>`, answerTimestamp, answerAuthorURL, answerAuthorName)
|
||||
newFilteredQuestion, err := extractQuestionData(doc, domain)
|
||||
if err != nil {
|
||||
c.HTML(500, "home.html", gin.H{
|
||||
"errorMessage": "Failed to extract question data",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// get the timestamp and author
|
||||
|
||||
answers = append(answers, template.HTML(answerBodyHTML))
|
||||
})
|
||||
answers, err := extractAnswersData(doc, domain)
|
||||
if err != nil {
|
||||
c.HTML(500, "home.html", gin.H{
|
||||
"errorMessage": "Failed to extract answer data",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
imagePolicy := "'self' https:"
|
||||
|
||||
@@ -149,16 +105,168 @@ func ViewQuestion(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.HTML(200, "question.html", gin.H{
|
||||
"title": questionText,
|
||||
"body": template.HTML(questionBodyParentHTML),
|
||||
"timestamp": questionTimestamp,
|
||||
"author": questionAuthor,
|
||||
"authorURL": questionAuthorURL,
|
||||
"answers": answers,
|
||||
"imagePolicy": imagePolicy,
|
||||
"shortenedBody": questionBodyParent.Text()[0:50],
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"currentUrl": fmt.Sprintf("%s/questions/%s/%s", os.Getenv("APP_URL"), questionId, questionTitle),
|
||||
"question": newFilteredQuestion,
|
||||
"answers": answers,
|
||||
"imagePolicy": imagePolicy,
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"currentUrl": fmt.Sprintf("%s%s", os.Getenv("APP_URL"), c.Request.URL.Path),
|
||||
"sortValue": params.SoSortValue,
|
||||
"domain": domain,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type viewQuestionInputs struct {
|
||||
QuestionID string
|
||||
QuestionTitle string
|
||||
SoSortValue string
|
||||
Sub string
|
||||
}
|
||||
|
||||
// parseAndValidateParameters consolidates the URL and query parameters into an easily-accessible struct.
|
||||
func parseAndValidateParameters(c *gin.Context) (inputs viewQuestionInputs, err error) {
|
||||
|
||||
questionId := c.Param("id")
|
||||
if _, err = strconv.Atoi(questionId); err != nil {
|
||||
c.HTML(400, "home.html", gin.H{
|
||||
"errorMessage": "Invalid question ID",
|
||||
"theme": c.MustGet("theme").(string),
|
||||
"version": config.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
inputs.QuestionID = questionId
|
||||
|
||||
sortValue := c.Query("sort_by")
|
||||
if sortValue == "" {
|
||||
sortValue = "votes"
|
||||
}
|
||||
|
||||
soSortValue, ok := soSortValues[sortValue]
|
||||
if !ok {
|
||||
soSortValue = soSortValues["votes"]
|
||||
}
|
||||
|
||||
inputs.SoSortValue = soSortValue
|
||||
|
||||
sub := c.Param("sub")
|
||||
|
||||
inputs.Sub = sub
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// fetchQuestionData sends the request to StackOverflow.
|
||||
func fetchQuestionData(soLink string) (resp *resty.Response, err error) {
|
||||
client := resty.New()
|
||||
resp, err = client.R().Get(soLink)
|
||||
return
|
||||
}
|
||||
|
||||
// extractQuestionData parses the HTML document and extracts question data.
|
||||
func extractQuestionData(doc *goquery.Document, domain string) (question types.FilteredQuestion, err error) {
|
||||
// Extract the question title.
|
||||
questionTextParent := doc.Find("h1.fs-headline1").First()
|
||||
question.Title = strings.TrimSpace(questionTextParent.Children().First().Text())
|
||||
|
||||
// Extract question tags.
|
||||
questionTags := utils.GetPostTags(doc.Find("div.post-layout").First())
|
||||
question.Tags = questionTags
|
||||
|
||||
// Extract and process the question body.
|
||||
questionBodyParent := doc.Find("div.s-prose").First()
|
||||
questionBodyParentHTML, err := questionBodyParent.Html()
|
||||
if err != nil {
|
||||
return question, err
|
||||
}
|
||||
question.Body = template.HTML(utils.ProcessHTMLBody(questionBodyParentHTML))
|
||||
|
||||
// Extract the shortened body description.
|
||||
shortenedBody := strings.TrimSpace(questionBodyParent.Text())
|
||||
shortenedBody = strings.ReplaceAll(shortenedBody, "\n", " ")
|
||||
if len(shortenedBody) > 50 {
|
||||
shortenedBody = shortenedBody[:50]
|
||||
}
|
||||
question.ShortenedBody = shortenedBody
|
||||
|
||||
// Extract question comments.
|
||||
comments := utils.FindAndReturnComments(questionBodyParentHTML, domain, doc.Find("div.post-layout").First())
|
||||
question.Comments = comments
|
||||
|
||||
// Extract question timestamp and author information.
|
||||
questionCard := doc.Find("div.postcell").First()
|
||||
extractMetadata(questionCard, &question, domain)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// extractMetadata extracts author and timestamp information from a given selection.
|
||||
func extractMetadata(selection *goquery.Selection, question *types.FilteredQuestion, domain string) {
|
||||
questionMetadata := selection.Find("div.user-info").First()
|
||||
question.Timestamp = questionMetadata.Find("span.relativetime").First().Text()
|
||||
|
||||
questionAuthorURL := "https://" + domain
|
||||
questionAuthor := selection.Find("div.post-signature.owner div.user-info div.user-details a").First()
|
||||
question.AuthorName = questionAuthor.Text()
|
||||
questionAuthorURL += questionAuthor.AttrOr("href", "")
|
||||
question.AuthorURL = questionAuthorURL
|
||||
|
||||
// Determine if the question has been edited and update author details accordingly.
|
||||
isQuestionEdited := selection.Find("a.js-gps-track").Text() == "edited"
|
||||
if isQuestionEdited {
|
||||
editedAuthor := questionMetadata.Find("a").Last()
|
||||
question.AuthorName = editedAuthor.Text()
|
||||
question.AuthorURL = "https://" + domain + editedAuthor.AttrOr("href", "")
|
||||
}
|
||||
}
|
||||
|
||||
// extractAnswersData parses the HTML document and extracts answers data.
|
||||
func extractAnswersData(doc *goquery.Document, domain string) ([]types.FilteredAnswer, error) {
|
||||
var answers []types.FilteredAnswer
|
||||
|
||||
// Iterate over each answer block.
|
||||
doc.Find("div.answer").Each(func(i int, s *goquery.Selection) {
|
||||
var answer types.FilteredAnswer
|
||||
|
||||
postLayout := s.Find("div.post-layout").First()
|
||||
|
||||
// Extract upvotes.
|
||||
voteCell := postLayout.Find("div.votecell").First()
|
||||
voteCount := html.EscapeString(voteCell.Find("div.js-vote-count").Text())
|
||||
answer.Upvotes = voteCount
|
||||
|
||||
// Check if the answer is accepted.
|
||||
answer.IsAccepted = s.HasClass("accepted-answer")
|
||||
|
||||
// Extract answer body and process it.
|
||||
answerCell := postLayout.Find("div.answercell").First()
|
||||
answerBody := answerCell.Find("div.s-prose").First()
|
||||
answerBodyHTML, _ := answerBody.Html()
|
||||
|
||||
// Process code blocks within the answer.
|
||||
processedAnswerBody := utils.ProcessHTMLBody(answerBodyHTML)
|
||||
answer.Body = template.HTML(html.UnescapeString(processedAnswerBody))
|
||||
|
||||
// Extract author information and timestamp.
|
||||
extractAnswerAuthorInfo(s, &answer, domain)
|
||||
|
||||
answers = append(answers, answer)
|
||||
})
|
||||
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
// extractAnswerAuthorInfo extracts the author name, URL, and timestamp from an answer block.
|
||||
// It directly mutates the answer.
|
||||
func extractAnswerAuthorInfo(selection *goquery.Selection, answer *types.FilteredAnswer, domain string) {
|
||||
authorDetails := selection.Find("div.post-signature").Last()
|
||||
|
||||
authorName := html.EscapeString(authorDetails.Find("div.user-details a").First().Text())
|
||||
authorURL := "https://" + domain + authorDetails.Find("div.user-details a").AttrOr("href", "")
|
||||
timestamp := html.EscapeString(authorDetails.Find("span.relativetime").Text())
|
||||
|
||||
answer.AuthorName = authorName
|
||||
answer.AuthorURL = authorURL
|
||||
answer.Timestamp = timestamp
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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")
|
||||
answerId := c.Param("answerId")
|
||||
|
||||
// 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/%s", id, answerId))
|
||||
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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package types
|
||||
|
||||
import "html/template"
|
||||
|
||||
type FilteredAnswer struct {
|
||||
ID string
|
||||
|
||||
Upvotes string
|
||||
IsAccepted bool
|
||||
|
||||
AuthorName string
|
||||
AuthorURL string
|
||||
|
||||
Timestamp string
|
||||
|
||||
Body template.HTML
|
||||
|
||||
Comments []FilteredComment
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user