diff --git a/.air.toml b/.air.toml index ad439e1..c9b54f3 100644 --- a/.air.toml +++ b/.air.toml @@ -23,7 +23,7 @@ tmp_dir = "tmp" rerun = false rerun_delay = 500 send_interrupt = false - stop_on_error = false + stop_on_error = true [color] app = "" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0f97088..cd460eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,34 +2,32 @@ name: Bug report about: 'Create a report to help us improve' title: '' -labels: '' +labels: 'bug' assignees: '' - --- Please make sure you're on the latest version before submitting. - # What's Happening? - + ## How to reproduce: - + ## Affected Platforms: -- [ ] macOS -- [ ] Windows -- [ ] Linux (Specify) -- [ ] iOS -- [ ] Android +- [ ] macOS +- [ ] Windows +- [ ] Linux (Specify) +- [ ] iOS +- [ ] Android -Version: +Version: ## Browser: -- [ ] Chromium-based (ex: Brave or Chrome) -- [ ] Webkit-based (ex: Safari) -- [ ] Gecko-based (ex: Firefox) +- [ ] Chromium-based (ex: Brave or Chrome) +- [ ] Webkit-based (ex: Safari) +- [ ] Gecko-based (ex: Firefox) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..287182e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,15 @@ +--- +name: Feature Request +about: 'Suggest a specific feature or enhancement' +title: '' +labels: 'enhancement' +assignees: '' +--- + +# What does the feature entail? + + + +# Why is this feature important? + + diff --git a/.github/ISSUE_TEMPLATE/new_instance.md b/.github/ISSUE_TEMPLATE/new_instance.md deleted file mode 100644 index 287c5f5..0000000 --- a/.github/ISSUE_TEMPLATE/new_instance.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -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): diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5b0d110..c7d3ceb 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -22,9 +22,13 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # 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 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -32,16 +36,17 @@ jobs: # 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 + uses: docker/metadata-action@v5 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 + uses: docker/build-push-action@v6 with: context: . push: true + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f70df7e..59c477b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine as build +FROM golang:1.22.1-alpine3.19 AS build WORKDIR /app @@ -9,17 +9,23 @@ RUN go mod download COPY . . -ENV CGO_ENABLED=0 +# Architecture and OS are set dynamically (by BuildKit) +ARG TARGETOS +ARG TARGETARCH +ENV CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH -RUN go build -o anonymousoverflow +RUN go build -o anonymousoverflow && go build -o healthcheck ./src/healthcheck FROM scratch COPY --from=build /app/anonymousoverflow /anonymousoverflow +COPY --from=build /app/healthcheck /healthcheck COPY templates /templates COPY public /public COPY --from=build /etc/ssl/certs /etc/ssl/certs +HEALTHCHECK --interval=60s --timeout=5s --start-period=2s --retries=3 CMD [ "/healthcheck","http://localhost:8080/healthz" ] + EXPOSE 8080 CMD ["/anonymousoverflow"] \ No newline at end of file diff --git a/README.md b/README.md index be1d57c..be2437d 100644 --- a/README.md +++ b/README.md @@ -6,45 +6,15 @@ This project is super lightweight by design. The UI is simple and the frontend i ## Screenshots -![Home](https://files.horizon.pics/e2b9275c-1409-4978-801b-de981a8d3ae9?a=1&mime1=image&mime2=png) +![Home](./docs/screenshots/home_dark.webp) -![Question](https://files.horizon.pics/0f6b0036-87f0-4acd-9a0f-936b5c397a73?a=1&mime1=image&mime2=png) +![Question](./docs/screenshots/question_dark.webp) -![Answer](https://files.horizon.pics/861ec510-644b-43f2-9439-0a2cae841422?a=1&mime1=image&mime2=png) +![Answer](./docs/screenshots/answers_light.webp) -## Clearnet Instances +## Instances -| 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) | -| [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/) | +Visit the [AnonymousOverflow Hub](https://aohub.httpjames.space) for a list of instances. ## Why use AnonymousOverflow over StackOverflow? @@ -73,7 +43,7 @@ StackOverflow has a cluttered UI that might distract you from the content you're 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. +The open-source [Proxy_Redirect](https://openuserjs.org/scripts/sjehuda/Proxy_Redirect) 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 diff --git a/config/version.go b/config/version.go index a8707b6..d2a1fa2 100644 --- a/config/version.go +++ b/config/version.go @@ -1,3 +1,3 @@ package config -var Version = "1.10.0" +var Version = "1.13.0" diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 7a9d7ac..2666abf 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,14 +1,12 @@ version: '3' services: - anonymousoverflow: - container_name: 'app' - build: - context: . - network: 'host' - environment: - - APP_URL=https://domain.com - - JWT_SIGNING_SECRET=secret - ports: - - '80:8080' - restart: 'always' \ No newline at end of file + 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' diff --git a/docs/screenshots/answers_light.webp b/docs/screenshots/answers_light.webp new file mode 100644 index 0000000..b07efb4 Binary files /dev/null and b/docs/screenshots/answers_light.webp differ diff --git a/docs/screenshots/home_dark.webp b/docs/screenshots/home_dark.webp new file mode 100644 index 0000000..d07324e Binary files /dev/null and b/docs/screenshots/home_dark.webp differ diff --git a/docs/screenshots/question_dark.webp b/docs/screenshots/question_dark.webp new file mode 100644 index 0000000..fbe20f8 Binary files /dev/null and b/docs/screenshots/question_dark.webp differ diff --git a/go.mod b/go.mod index 2545ecc..b5fb1e7 100644 --- a/go.mod +++ b/go.mod @@ -3,33 +3,65 @@ module anonymousoverflow go 1.19 require ( - github.com/PuerkitoBio/goquery v1.8.0 + github.com/PuerkitoBio/goquery v1.9.1 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 + github.com/gin-gonic/gin v1.10.0 + github.com/go-resty/resty/v2 v2.12.0 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/joho/godotenv v1.5.1 ) require ( - github.com/andybalholm/cascadia v1.3.1 // indirect - github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // 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/goccy/go-json v0.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/influxdata/influxdb-client-go/v2 v2.13.0 // indirect + github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // 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 + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect 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/ugorji/go/codec v1.2.8 // indirect - golang.org/x/crypto v0.4.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 + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/oapi-codegen/runtime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.5.3 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/tavsec/gin-healthcheck v1.6.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.mongodb.org/mongo-driver v1.15.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 017319e..4ec3994 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,113 @@ 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/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= 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/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= +github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= 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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/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-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= +github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/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/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 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= @@ -47,24 +118,40 @@ 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= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= +github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= 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/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 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/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 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= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -72,34 +159,122 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/tavsec/gin-healthcheck v1.6.2 h1:F89IFXXtYOy3p4gne8WFkos3r7vjMbE+R3C/v70dTW0= +github.com/tavsec/gin-healthcheck v1.6.2/go.mod h1:VcZ4f44KqMnwbzRBrr7VYni2GmkMErd/44QuM5Dy/YI= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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= @@ -111,3 +286,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C 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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/instances.json b/instances.json new file mode 100644 index 0000000..5d15b83 --- /dev/null +++ b/instances.json @@ -0,0 +1,232 @@ +{ + "clearnet": [ + { + "url": "https://code.whatever.social", + "regions": ["Canada", "United States"], + "operators": ["https://whatever.social", "https://httpjames.space"] + }, + { + "url": "https://ao.vern.cc", + "regions": ["United States"], + "operators": ["https://vern.cc"] + }, + { + "url": "https://overflow.smnz.de", + "regions": ["Germany"], + "operators": ["https://smnz.de"] + }, + { + "url": "https://overflow.lunar.icu", + "regions": ["Germany"], + "operators": ["https://lunar.icu/"] + }, + { + "url": "https://overflow.adminforge.de/", + "regions": ["Germany"], + "operators": ["https://adminforge.de/"] + }, + { + "url": "https://overflow.hostux.net/", + "regions": ["France"], + "operators": ["https://hostux.net/"] + }, + { + "url": "https://overflow.projectsegfau.lt/", + "regions": ["United States", "Germany", "India"], + "operators": ["https://projectsegfau.lt/"] + }, + { + "url": "https://code.xbdm.fun", + "regions": ["Germany"], + "operators": ["https://xbdm.fun"] + }, + { + "url": "https://overflow.fascinated.cc/", + "regions": ["Germany"], + "operators": ["https://fascinated.cc/"] + }, + { + "url": "https://ao.bloat.cat", + "regions": ["Germany"], + "operators": ["https://bloat.cat"] + }, + { + "url": "https://anonoverflow.frontendfriendly.xyz/", + "regions": ["United States"], + "operators": ["https://frontendfriendly.xyz/"] + }, + { + "url": "https://ao.owo.si/", + "regions": ["Germany"], + "operators": ["https://owo.si/"] + }, + { + "url": "https://overflow.datura.network/", + "regions": ["Germany"], + "operators": ["https://datura.network"] + }, + { + "url": "https://overflow.freedit.eu", + "regions": ["United States"], + "operators": ["https://freedit.eu"] + }, + { + "url": "https://ao.rootdo.com", + "regions": ["Germany"], + "operators": ["https://rootdo.com"] + }, + { + "url": "https://anonoverflow.hyperreal.coffee", + "regions": ["Germany"], + "operators": ["https://hyperreal.coffee"] + }, + { + "url": "https://o.sudovanilla.org", + "regions": ["United States"], + "operators": ["https://sudovanilla.org"] + }, + { + "url": "https://anonymousoverflow.privacyfucking.rocks/", + "regions": ["Germany"], + "operators": ["https://privacyfucking.rocks"] + }, + { + "url": "https://exchange.seitan-ayoub.lol", + "regions": ["Germany"], + "operators": ["https://seitan-ayoub.lol"] + }, + { + "url": "https://overflow.r4fo.com", + "regions": ["The Netherlands"], + "operators": ["https://r4fo.com"] + }, + { + "url": "https://overflow.ducks.party", + "regions": ["The Netherlands"], + "operators": ["https://ducks.party"] + }, + { + "url": "https://ao.ngn.tf", + "regions": ["Turkey"], + "operators": ["https://ngn.tf"] + }, + { + "url": "https://overflow.snine.nl", + "regions": ["The Netherlands"], + "operators": ["https://snine.nl"] + }, + { + "url": "https://anonymousoverflow.privacyredirect.com", + "regions": ["Finland"], + "operators": ["https://privacyredirect.com/"] + }, + { + "url": "https://soflow.nerdvpn.de", + "regions": ["Ukraine"], + "operators": ["https://nerdvpn.de"] + }, + { + "url": "https://overflow.einfachzocken.eu/", + "regions": ["Germany"], + "operators": ["https://einfachzocken.eu"] + }, + { + "url": "https://overflow.seasi.dev/", + "regions": ["Singapore"], + "operators": ["https://seasi.dev/"] + }, + { + "url": "https://anonymousoverflow.catsarch.com", + "regions": ["United States"], + "operators": ["https://catsarch.com"] + }, + { + "url": "https://overflow.darkness.services/", + "regions": ["United States"], + "operators": ["https://zzz.darkness.services"] + }, + { + "url": "https://anonflow.aketawi.space/", + "regions": ["Russia"], + "operators": ["https://www.aketawi.space/"] + }, + { + "url": "https://ao.bunk.lol", + "regions": ["Iceland"], + "operators": ["https://bunk.lol"] + }, + { + "url": "https://o.iii.st/", + "regions": ["Germany"], + "operators": ["https://iii.st/"] + }, + { + "url": "https://overflow.canine.tools/", + "regions": ["United States"], + "operators": ["https://canine.tools/"] + } + ], + + "onion": [ + { + "url": "http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion", + "regions": ["United States"], + "operators": ["https://vern.cc"] + }, + { + "url": "http://overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/", + "regions": ["Germany"], + "operators": ["https://projectsegfau.lt/"] + }, + { + "url": "http://overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/", + "regions": ["Germany"], + "operators": ["https://datura.network"] + }, + { + "url": "http://ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion/", + "regions": ["Germany"], + "operators": ["https://owo.si/"] + }, + { + "url": "http://overflow.r4focoma7gu2zdwwcjjad47ysxt634lg73sxmdbkdozanwqslho5ohyd.onion", + "regions": ["The Netherlands"], + "operators": ["https://r4fo.com"] + }, + { + "url": "http://anonymousoverflow.catsarchywsyuss6jdxlypsw5dc7owd5u5tr6bujxb7o6xw2hipqehyd.onion/", + "regions": ["United States"], + "operators": ["https://catsarch.com"] + }, + { + "url": "http://overflow.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion/", + "regions": ["United States"], + "operators": [ + "http://darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion/" + ] + }, + { + "url": "http://o.zx56doutynmbgezxtpccduajwcblzx7fgio2yuy57a3jingco2c6fvqd.onion/", + "regions": ["Germany"], + "operators": ["https://iii.st/"] + } + ], + + "i2p": [ + { + "url": "http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p", + "regions": ["United States"], + "operators": ["https://vern.cc"] + }, + { + "url": "http://ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p/", + "regions": ["Germany"], + "operators": ["https://owo.si/"] + }, + { + "url": "http://ocp7zhdsbl2mjabv5ma5jvbzg2dqzglieayjvyj4j2r7qvsqlboa.b32.i2p/", + "regions": ["United States"], + "operators": ["https://catsarch.com"] + } + ] +} diff --git a/main.go b/main.go index 8b16302..c9cc96c 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,9 @@ import ( "os" "github.com/gin-gonic/gin" + healthcheck "github.com/tavsec/gin-healthcheck" + "github.com/tavsec/gin-healthcheck/checks" + "github.com/tavsec/gin-healthcheck/config" ) func main() { @@ -35,7 +38,6 @@ func main() { r.Use(gin.Recovery()) r.Use(middleware.XssPreventionHeaders()) - r.Use(middleware.NoCacheMiddleware()) r.Use(middleware.OptionsMiddleware()) r.Use(middleware.Ratelimit()) @@ -51,6 +53,26 @@ func main() { r.POST("/", routes.PostHome) r.GET("/a/:id", routes.RedirectShortenedOverflowURL) + r.GET("/a/:id/:answerId", routes.RedirectShortenedOverflowURL) + r.GET("/q/:id", routes.RedirectShortenedOverflowURL) + r.GET("/q/:id/:answerId", routes.RedirectShortenedOverflowURL) + + exchangeRouter := r.Group("/exchange/:sub") + { + exchangeRouter.GET("/questions/:id/:title", routes.ViewQuestion) + exchangeRouter.GET("/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"))) + }) + exchangeRouter.GET("/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"))) + }) + exchangeRouter.GET("/q/:id/:answerId", routes.RedirectShortenedOverflowURL) + exchangeRouter.GET("/q/:id", routes.RedirectShortenedOverflowURL) + exchangeRouter.GET("/a/:id/:answerId", routes.RedirectShortenedOverflowURL) + exchangeRouter.GET("/a/:id", routes.RedirectShortenedOverflowURL) + } r.GET("/questions/:id", func(c *gin.Context) { // redirect user to the question with the title @@ -61,17 +83,14 @@ func main() { // 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.GET("/version", routes.GetVersion) + + soPingCheck := checks.NewPingCheck("https://stackoverflow.com", "GET", 5000, nil, nil) + sePingCheck := checks.NewPingCheck("https://stackexchange.com", "GET", 5000, nil, nil) + healthcheck.New(r, config.DefaultConfig(), []checks.Check{soPingCheck, sePingCheck}) + r.Run(fmt.Sprintf("%s:%s", host, port)) } diff --git a/public/codecircles.svg b/public/codecircles.svg new file mode 100644 index 0000000..58fd99b --- /dev/null +++ b/public/codecircles.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/codecircles.webp b/public/codecircles.webp index 7744569..01f747c 100644 Binary files a/public/codecircles.webp and b/public/codecircles.webp differ diff --git a/public/globals.css b/public/globals.css index afe09ba..1592648 100644 --- a/public/globals.css +++ b/public/globals.css @@ -1,36 +1,67 @@ :root { + --code-bg: #36383d; + --code-fg: #ffffff; +} + +:root, +[data-theme="dark"] { --main-bg: #1b1f26; --text-color: #fff; --muted-text-color: #b3b3b3; - --code-bg: #36383d; --input-bg: #2b303b; --input-bg-hover: #3b404b; - --meta-bg: rgb(82, 82, 98); + --meta-bg: #525262; --divider-color: #42464e; --link-color: #92adff; } -[data-theme='light'] { - --main-bg: rgb(219, 219, 219); +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) { + --main-bg: #dbdbdb; + --text-color: #000; + --muted-text-color: #636363; + --input-bg: #bcbcbc; + --input-bg-hover: #a8a8a8; + --meta-bg: #aaa8a8; + --divider-color: #b5b5b5; + --link-color: #335ad0; + } +} + +[data-theme="light"] { + --main-bg: #dbdbdb; --text-color: #000; --muted-text-color: #636363; - --code-bg: #36383d; - --input-bg: rgb(188, 188, 188); - --input-bg-hover: rgb(168, 168, 168); - --meta-bg: rgb(170, 168, 168); + --input-bg: #bcbcbc; + --input-bg-hover: #a8a8a8; + --meta-bg: #aaa8a8; --divider-color: #b5b5b5; --link-color: #335ad0; } a { color: var(--link-color); + word-wrap: break-word; } html { + margin: auto; + max-width: 40rem; +} + +@media only screen and (max-width: calc(40rem + 2rem)) { + body { + padding-left: 1rem; + padding-right: 1rem; + } +} + +body { + background-color: var(--main-bg); + color: var(--text-color); + font-family: sans-serif; + margin: 0; - padding: 0; - width: 100vw; - height: 100vh; } .icon { @@ -50,3 +81,15 @@ html { details { cursor: pointer; } + +.d-flex { + display: flex; +} + +.fd-column { + flex-direction: column; +} + +.fw-nowrap { + flex-wrap: nowrap; +} diff --git a/public/home.css b/public/home.css index e499a09..d947c81 100644 --- a/public/home.css +++ b/public/home.css @@ -1,21 +1,10 @@ body { - background-color: var(--main-bg); - font-family: sans-serif; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - width: 100vw; - - margin: 0; - - color: var(--text-color); } .container { - width: 40rem; + margin: auto; } .footer { @@ -44,7 +33,7 @@ body { .view-input:focus { outline: none; - border: 2px solid rgb(168, 168, 168); + border: 2px solid #a8a8a8; } .view-button { @@ -71,7 +60,7 @@ body { } .error { - background-color: rgb(255, 129, 129); + background-color: #ff8181; } .error, @@ -104,10 +93,3 @@ body { width: 2rem; height: 2rem; } - -@media screen and (max-width: 800px) { - body { - padding: 1rem; - box-sizing: border-box; - } -} diff --git a/public/question.css b/public/question.css index ada56f3..a9b8bae 100644 --- a/public/question.css +++ b/public/question.css @@ -1,35 +1,5 @@ body { - margin: 0; - width: 100vw; - height: 100vh; - - overflow-x: hidden; - - background-color: var(--main-bg); - color: var(--text-color); - font-family: sans-serif; - - display: flex; - justify-content: center; - - padding-left: 5rem; - padding-right: 5rem; padding-top: 2rem; - - box-sizing: border-box; -} - -@media (orientation: landscape) { - .parent { - max-width: 50%; - width: fit-content; - } -} -@media (orientation: portrait) { - .parent { - max-width: 90%; - width: fit-content; - } } .header { @@ -55,14 +25,14 @@ code { background-color: var(--code-bg); padding: 0.15rem; border-radius: 5px; - color: white; + color: var(--code-fg); } pre { background-color: var(--code-bg); padding: 1rem; border-radius: 5px; - color: var(--text-color); + color: var(--code-fg); overflow-x: auto; line-height: 1.35; } @@ -162,6 +132,7 @@ img { .answers-header { display: flex; + flex-wrap: wrap; justify-content: space-between; align-items: center; } @@ -195,10 +166,3 @@ img { justify-content: center; align-items: center; } - -@media only screen and (max-width: 800px) { - body { - padding-left: 1rem; - padding-right: 1rem; - } -} diff --git a/src/healthcheck/healthcheck.go b/src/healthcheck/healthcheck.go new file mode 100644 index 0000000..24b2318 --- /dev/null +++ b/src/healthcheck/healthcheck.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" +) + +func main() { + if len(os.Args) < 2 { + log.Fatal("Expected URL as command-line argument") + os.Exit(1) + } + url := os.Args[1] + fmt.Println(url) + if _, err := http.Get(url); err != nil { + os.Exit(1) + } +} diff --git a/src/middleware/noCache.go b/src/middleware/noCache.go deleted file mode 100644 index 0b5c8a5..0000000 --- a/src/middleware/noCache.go +++ /dev/null @@ -1,12 +0,0 @@ -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() - } -} diff --git a/src/middleware/options.go b/src/middleware/options.go index 95b9d94..a0fea14 100644 --- a/src/middleware/options.go +++ b/src/middleware/options.go @@ -7,7 +7,6 @@ import ( func OptionsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Set("disable_images", false) - c.Set("theme", "dark") imagesCookie, err := c.Cookie("disable_images") if err == nil { @@ -16,13 +15,6 @@ func OptionsMiddleware() gin.HandlerFunc { } } - themeCookie, err := c.Cookie("theme") - if err == nil { - if themeCookie == "light" { - c.Set("theme", "light") - } - } - c.Next() } } diff --git a/src/middleware/ratelimit.go b/src/middleware/ratelimit.go index bc0bf47..d83c901 100644 --- a/src/middleware/ratelimit.go +++ b/src/middleware/ratelimit.go @@ -47,7 +47,6 @@ func Ratelimit() gin.HandlerFunc { if val.(int) > 30 { 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() diff --git a/src/routes/home.go b/src/routes/home.go index 27b511a..78202e1 100644 --- a/src/routes/home.go +++ b/src/routes/home.go @@ -2,6 +2,7 @@ package routes import ( "anonymousoverflow/config" + "anonymousoverflow/src/utils" "fmt" "regexp" "strings" @@ -10,9 +11,10 @@ import ( ) func GetHome(c *gin.Context) { + theme := utils.GetThemeFromEnv() c.HTML(200, "home.html", gin.H{ "version": config.Version, - "theme": c.MustGet("theme").(string), + "theme": theme, }) } @@ -20,7 +22,41 @@ type urlConversionRequest struct { URL string `form:"url" binding:"required"` } -var stackExchangeRegex = regexp.MustCompile(`https://(.+).stackexchange.com/questions/`) +var coreRegex = regexp.MustCompile(`(?:https?://)?(?:www\.)?([^/]+)(/(?:questions|q|a)/.+)`) + +// Will return `nil` if `rawUrl` is invalid. +func translateUrl(rawUrl string) string { + coreMatches := coreRegex.FindStringSubmatch(rawUrl) + if coreMatches == nil { + return "" + } + + domain := coreMatches[1] + rest := coreMatches[2] + + exchange := "" + if domain == "stackoverflow.com" { + // No exchange parameter needed. + } else if sub, found := strings.CutSuffix(domain, ".stackexchange.com"); found { + if sub == "" { + return "" + } else if strings.Contains(sub, ".") { + // Anything containing dots is interpreted as a full domain, so we use the correct full domain. + exchange = domain + } else { + exchange = sub + } + } else { + exchange = domain + } + + // Ensure we properly format the return string to avoid double slashes + if exchange == "" { + return rest + } else { + return fmt.Sprintf("/exchange/%s%s", exchange, rest) + } +} func PostHome(c *gin.Context) { body := urlConversionRequest{} @@ -28,36 +64,20 @@ func PostHome(c *gin.Context) { if err := c.ShouldBind(&body); err != nil { c.HTML(400, "home.html", gin.H{ "errorMessage": "Invalid request body", - "theme": c.MustGet("theme").(string), }) return } - soLink := body.URL + translated := translateUrl(body.URL) - // remove the www. - soLink = strings.ReplaceAll(soLink, "www.", "") - - // validate URL - isStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/questions/") - isShortenedStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/a/") - isStackExchange := stackExchangeRegex.MatchString(soLink) - if !isStackExchange && !isStackOverflow && !isShortenedStackOverflow { + if translated == "" { + theme := utils.GetThemeFromEnv() c.HTML(400, "home.html", gin.H{ "errorMessage": "Invalid stack overflow/exchange URL", - "theme": c.MustGet("theme").(string), + "theme": theme, }) return } - // 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)))) + c.Redirect(302, translated) } diff --git a/src/routes/home_test.go b/src/routes/home_test.go new file mode 100644 index 0000000..eda6d64 --- /dev/null +++ b/src/routes/home_test.go @@ -0,0 +1,32 @@ +package routes + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTranslateUrl(t *testing.T) { + assert := assert.New(t) + + // Test with a Valid StackOverflow URL + assert.Equal("/questions/example-question", translateUrl("https://stackoverflow.com/questions/example-question"), "StackOverflow URL should not be modified") + + // Test with Complex Subdomain + assert.Equal("/exchange/meta.math.stackexchange.com/q/example-question", translateUrl("https://meta.math.stackexchange.com/q/example-question"), "Complex StackExchange subdomain should be used as full exchange") + + // Test with Non-StackExchange Domain + assert.Equal("/exchange/example.com/questions/example-question", translateUrl("https://example.com/questions/example-question"), "Non-StackExchange domain should be detected as exchange") + + // Test with Invalid URL + assert.Equal("", translateUrl("This is not a URL"), "Invalid URL should return an empty string") + + // Test with Empty String + assert.Equal("", translateUrl(""), "Empty string should return an empty string") + + // Test with Missing Path + assert.Equal("", translateUrl("https://stackoverflow.com"), "URL without path should return an empty string") + + // Test with Valid URL but Root Domain for StackExchange + assert.Equal("", translateUrl("https://stackexchange.com"), "Root StackExchange domain without subdomain should return an empty string") +} diff --git a/src/routes/options.go b/src/routes/options.go index a665be2..c0c8718 100644 --- a/src/routes/options.go +++ b/src/routes/options.go @@ -2,9 +2,8 @@ package routes import ( "anonymousoverflow/config" + "anonymousoverflow/src/utils" "fmt" - "os" - "strings" "github.com/gin-gonic/gin" ) @@ -19,28 +18,12 @@ 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) + theme := utils.GetThemeFromEnv() c.HTML(200, "home.html", gin.H{ "successMessage": "Images are now " + text, - "theme": c.MustGet("theme").(string), "version": config.Version, + "theme": theme, }) - - 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 !strings.HasPrefix(redirectUrl, os.Getenv("APP_URL")) { - redirectUrl = os.Getenv("APP_URL") - } - - c.Redirect(302, redirectUrl) - default: c.String(400, "400 Bad Request") } diff --git a/src/routes/question.go b/src/routes/question.go index 9a81bc0..3b6274b 100644 --- a/src/routes/question.go +++ b/src/routes/question.go @@ -29,55 +29,36 @@ var soSortValues = map[string]string{ } func ViewQuestion(c *gin.Context) { - client := resty.New() 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 } - questionTitle := c.Param("title") - - sortValue := c.Query("sort_by") - if sortValue == "" { - sortValue = "votes" + params, err := parseAndValidateParameters(c) + if err != nil { + return } - 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) + if strings.Contains(params.Sub, ".") { + domain = params.Sub + } else if params.Sub != "" { + domain = fmt.Sprintf("%s.stackexchange.com", params.Sub) } - soLink := fmt.Sprintf("https://%s/questions/%s/%s?answertab=%s", domain, questionId, questionTitle, soSortValue) + soLink := fmt.Sprintf("https://%s/questions/%s/%s?answertab=%s", domain, questionId, params.QuestionTitle, params.SoSortValue) - resp, err := client.R().Get(soLink) - if err != nil { - c.HTML(500, "home.html", gin.H{ - "errorMessage": "Unable to fetch question data", - "theme": c.MustGet("theme").(string), - "version": config.Version, - }) - return - } - defer resp.RawResponse.Body.Close() + resp, err := fetchQuestionData(soLink) if resp.StatusCode() != 200 { c.HTML(500, "home.html", gin.H{ - "errorMessage": "Received a non-OK status code", - "theme": c.MustGet("theme").(string), + "errorMessage": fmt.Sprintf("Received a non-OK status code %d", resp.StatusCode()), "version": config.Version, }) return @@ -91,187 +72,28 @@ func ViewQuestion(c *gin.Context) { if err != nil { c.HTML(500, "home.html", gin.H{ "errorMessage": "Unable to parse question data", - "theme": c.MustGet("theme").(string), "version": config.Version, }) return } - newFilteredQuestion := types.FilteredQuestion{} - - questionTextParent := doc.Find("h1.fs-headline1") - - questionText := questionTextParent.Children().First().Text() - - newFilteredQuestion.Title = questionText - - questionPostLayout := doc.Find("div.post-layout").First() - - questionTags := utils.GetPostTags(questionPostLayout) - newFilteredQuestion.Tags = questionTags - - questionBodyParent := doc.Find("div.s-prose") - - questionBodyParentHTML, err := questionBodyParent.Html() + newFilteredQuestion, err := extractQuestionData(doc, domain) if err != nil { c.HTML(500, "home.html", gin.H{ - "errorMessage": "Unable to parse question body", - "theme": c.MustGet("theme").(string), + "errorMessage": "Failed to extract question data", "version": config.Version, }) return } - newFilteredQuestion.Body = template.HTML(utils.ReplaceImgTags(questionBodyParentHTML)) - - questionBodyText := questionBodyParent.Text() - - // remove all whitespace to create the shortened body desc - shortenedBody := strings.TrimSpace(questionBodyText) - - // remove all newlines - shortenedBody = strings.ReplaceAll(shortenedBody, "\n", " ") - - // get the first 50 chars - shortenedBody = shortenedBody[:50] - - newFilteredQuestion.ShortenedBody = shortenedBody - - comments := utils.FindAndReturnComments(questionBodyParentHTML, domain, questionPostLayout) - newFilteredQuestion.Comments = comments - - // parse any code blocks and highlight them - answerCodeBlocks := questionCodeBlockRegex.FindAllString(questionBodyParentHTML, -1) - for _, codeBlock := range answerCodeBlocks { - codeBlock = utils.StripBlockTags(codeBlock) - - // syntax highlight - highlightedCodeBlock := utils.HighlightSyntaxViaContent(codeBlock) - - // replace the code block with the highlighted code block - questionBodyParentHTML = strings.Replace(questionBodyParentHTML, codeBlock, highlightedCodeBlock, 1) - } - - 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 - } - }) - - newFilteredQuestion.Timestamp = questionTimestamp - - userDetails := questionMetadata.Find("div.user-details") - - questionAuthor := "" - 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) { - // if question has been edited, the author is the second element. - - if isQuestionEdited { - if i == 1 { - questionAuthor = s.Text() - questionAuthorURL += s.AttrOr("href", "") - return - } - } else { - // otherwise it's the first element - if i == 0 { - questionAuthor = s.Text() - questionAuthorURL += s.AttrOr("href", "") - return - } - } - }) - - newFilteredQuestion.AuthorName = questionAuthor - newFilteredQuestion.AuthorURL = questionAuthorURL - - answers := []types.FilteredAnswer{} - - doc.Find("div.answer").Each(func(i int, s *goquery.Selection) { - - newFilteredAnswer := types.FilteredAnswer{} - - 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 := html.EscapeString(voteCell.Find("div.js-vote-count").Text()) - - newFilteredAnswer.Upvotes = voteCount - newFilteredAnswer.IsAccepted = s.HasClass("accepted-answer") - - answerFooter := s.Find("div.mt24") - - 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 { - questionAuthor := answerAuthorDetails.Find("a").First() - answerAuthorName = html.EscapeString(questionAuthor.Text()) - answerAuthorURL += html.EscapeString(questionAuthor.AttrOr("href", "")) - } - - answerTimestamp = html.EscapeString(s.Find("span.relativetime").Text()) + answers, err := extractAnswersData(doc, domain) + if err != nil { + c.HTML(500, "home.html", gin.H{ + "errorMessage": "Failed to extract answer data", + "version": config.Version, }) - - answerId, _ := s.Attr("data-answerid") - newFilteredAnswer.ID = answerId - - newFilteredAnswer.AuthorName = answerAuthorName - newFilteredAnswer.AuthorURL = answerAuthorURL - newFilteredAnswer.Timestamp = answerTimestamp - - // parse any code blocks and highlight them - answerCodeBlocks := codeBlockRegex.FindAllString(answerBodyHTML, -1) - for _, codeBlock := range answerCodeBlocks { - codeBlock = utils.StripBlockTags(codeBlock) - - // syntax highlight - highlightedCodeBlock := utils.HighlightSyntaxViaContent(codeBlock) - - // replace the code block with the highlighted code block - answerBodyHTML = strings.Replace(answerBodyHTML, codeBlock, highlightedCodeBlock, 1) - } - - comments = utils.FindAndReturnComments(answerBodyHTML, domain, postLayout) - - newFilteredAnswer.Comments = comments - newFilteredAnswer.Body = template.HTML(utils.ReplaceImgTags(answerBodyHTML)) - - answers = append(answers, newFilteredAnswer) - }) + return + } imagePolicy := "'self' https:" @@ -279,14 +101,174 @@ func ViewQuestion(c *gin.Context) { imagePolicy = "'self'" } + theme := utils.GetThemeFromEnv() + c.HTML(200, "question.html", gin.H{ "question": newFilteredQuestion, "answers": answers, "imagePolicy": imagePolicy, - "theme": c.MustGet("theme").(string), - "currentUrl": fmt.Sprintf("%s/questions/%s/%s", os.Getenv("APP_URL"), questionId, questionTitle), - "sortValue": sortValue, + "currentUrl": fmt.Sprintf("%s%s", os.Getenv("APP_URL"), c.Request.URL.Path), + "sortValue": params.SoSortValue, "domain": domain, + "theme": theme, }) } + +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", + "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 + + answer.ID = s.AttrOr("data-answerid", "") + + 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(processedAnswerBody) + + answer.Comments = utils.FindAndReturnComments(answerBodyHTML, domain, postLayout) + + // 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 +} diff --git a/src/routes/shortened.go b/src/routes/shortened.go index ed1ad1e..494b5bc 100644 --- a/src/routes/shortened.go +++ b/src/routes/shortened.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "strings" "github.com/gin-gonic/gin" "github.com/go-resty/resty/v2" @@ -11,6 +12,8 @@ import ( func RedirectShortenedOverflowURL(c *gin.Context) { id := c.Param("id") + answerId := c.Param("answerId") + sub := c.Param("sub") // fetch the stack overflow URL client := resty.New() @@ -20,11 +23,16 @@ func RedirectShortenedOverflowURL(c *gin.Context) { }), ) - resp, err := client.R().Get(fmt.Sprintf("https://www.stackoverflow.com/a/%s", id)) + domain := "www.stackoverflow.com" + if strings.Contains(sub, ".") { + domain = sub + } else if sub != "" { + domain = fmt.Sprintf("%s.stackexchange.com", sub) + } + resp, err := client.R().Get(fmt.Sprintf("https://%s/a/%s/%s", domain, 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 } @@ -32,7 +40,6 @@ func RedirectShortenedOverflowURL(c *gin.Context) { 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 } @@ -40,5 +47,10 @@ func RedirectShortenedOverflowURL(c *gin.Context) { // get the redirect URL location := resp.Header().Get("Location") - c.Redirect(302, fmt.Sprintf("%s%s", os.Getenv("APP_URL"), location)) + redirectPrefix := os.Getenv("APP_URL") + if sub != "" { + redirectPrefix += fmt.Sprintf("/exchange/%s", sub) + } + + c.Redirect(302, fmt.Sprintf("%s%s", redirectPrefix, location)) } diff --git a/src/routes/version.go b/src/routes/version.go new file mode 100644 index 0000000..d7dc5c5 --- /dev/null +++ b/src/routes/version.go @@ -0,0 +1,10 @@ +package routes + +import ( + "anonymousoverflow/config" + "github.com/gin-gonic/gin" +) + +func GetVersion(c *gin.Context) { + c.String(200, config.Version) +} diff --git a/src/utils/comments.go b/src/utils/comments.go index d668115..cc935e4 100644 --- a/src/utils/comments.go +++ b/src/utils/comments.go @@ -50,7 +50,7 @@ func FindAndReturnComments(inHtml, domain string, postLayout *goquery.Selection) commentTimestamp := commentBody.Find("span.relativetime-clean").Text() newFilteredComment := types.FilteredComment{ - Text: template.HTML(commentCopy), + Text: template.HTML(ProcessHTMLBody(commentCopy)), Timestamp: commentTimestamp, AuthorName: commentAuthor.Text(), AuthorURL: commentAuthorURL, diff --git a/src/utils/links.go b/src/utils/links.go new file mode 100644 index 0000000..56ad79a --- /dev/null +++ b/src/utils/links.go @@ -0,0 +1,45 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +// stackOverflowLinkQualifierRegex matches all anchor elements that meet the following conditions: +// * must be an anchor element +// * the anchor element must have a pathname beginning with /q or /questions +// * if there is a host, it must be stackoverflow.com or a subdomain +var stackOverflowLinkQualifierRegex = regexp.MustCompile(`]*href="(?:https?://(?:www\.)?(?:\w+\.)*(?:stackoverflow|stackexchange)\.com)?/(?:q|questions)/[^"]*"[^>]*>.*?`) + +func ReplaceStackOverflowLinks(html string) string { + return stackOverflowLinkQualifierRegex.ReplaceAllStringFunc(html, func(match string) string { + // Extract the href attribute value from the anchor tag + hrefRegex := regexp.MustCompile(`href="([^"]*)"`) + hrefMatch := hrefRegex.FindStringSubmatch(match) + if len(hrefMatch) < 2 { + return match + } + href := hrefMatch[1] + + // Parse the URL + url, err := url.Parse(href) + if err != nil { + return match + } + + newUrl := url.String() + + // Check if the host is a subdomain + parts := strings.Split(url.Host, ".") + if len(parts) > 2 { + // Prepend the subdomain to the path + url.Path = "/exchange/" + parts[0] + url.Path + } + + newUrl = url.Path + url.RawQuery + url.Fragment + + // Replace the href attribute value in the anchor tag + return strings.Replace(match, hrefMatch[1], newUrl, 1) + }) +} \ No newline at end of file diff --git a/src/utils/links_test.go b/src/utils/links_test.go new file mode 100644 index 0000000..5279fe8 --- /dev/null +++ b/src/utils/links_test.go @@ -0,0 +1,49 @@ +package utils + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +var sampleInput = `
+
+
+
+
+ This question already has answers here: + +
+
+
+
+ + + +
Closed 4 years ago.
+
` + +func TestReplaceStackOverflowLinks(t *testing.T) { + replacedLinks := ReplaceStackOverflowLinks(sampleInput) + + fmt.Println(replacedLinks) + + assert.False(t, strings.Contains(replacedLinks, "stackoverflow.com")) + assert.False(t, strings.Contains(replacedLinks, "stackexchange.com")) +} diff --git a/src/utils/process.go b/src/utils/process.go new file mode 100644 index 0000000..1a00435 --- /dev/null +++ b/src/utils/process.go @@ -0,0 +1,9 @@ +package utils + +// ProcessHTMLBody runs HTML through the various preparation functions. +func ProcessHTMLBody(bodyHTML string) string { + highlightedBody := HighlightCodeBlocks(bodyHTML) + imageProxiedBody := ReplaceImgTags(highlightedBody) + stackOverflowLinksReplacedBody := ReplaceStackOverflowLinks(imageProxiedBody) + return stackOverflowLinksReplacedBody +} diff --git a/src/utils/syntax.go b/src/utils/syntax.go index cb4c4ea..369f114 100644 --- a/src/utils/syntax.go +++ b/src/utils/syntax.go @@ -12,17 +12,19 @@ import ( "github.com/alecthomas/chroma/styles" ) -func HighlightSyntaxViaContent(content string) (htmlOut string) { +// highlightSyntaxViaContent uses Chroma to lex code content and apply the appropriate tokenizer engine. +// If it can't find one, it defaults to JavaScript syntax highlighting. +func highlightSyntaxViaContent(content string, lang string) (htmlOut string) { content = html.UnescapeString(content) fallbackOut := html.EscapeString(content) - // identify the language - lexer := lexers.Analyse(content) + lexer := lexers.Get(lang) if lexer == nil { - // unable to identify, so just return the wrapped content - htmlOut = fallbackOut - return + lexer = lexers.Analyse(content) + } + if lexer == nil { + lexer = lexers.Get(".js") } style := styles.Get("xcode") @@ -54,7 +56,9 @@ func HighlightSyntaxViaContent(content string) (htmlOut string) { var preClassRegex = regexp.MustCompile(`(?s)
`)
 
-func StripBlockTags(content string) (result string) {
+// stripBlockTags takes an extracted code block from HTML and strips it of its pre and code tags.
+// What's returned is just the code.
+func stripBlockTags(content string) (result string) {
 	// strip all "" tags
 	content = strings.Replace(content, "", "", -1)
 	content = strings.Replace(content, "", "", -1)
@@ -68,3 +72,27 @@ func StripBlockTags(content string) (result string) {
 
 	return
 }
+
+var codeBlockRegex = regexp.MustCompile(`(?s)]+?lang-(.+?)[\s"'])?.*?>(.*?)<\/code><\/pre>`)
+
+// HighlightCodeBlocks uses both highlightSyntaxViaContent stripCodeBlocks and returns the newly highlighted code HTML.
+func HighlightCodeBlocks(html string) string {
+	// Replace each code block with the highlighted version
+	highlightedHTML := codeBlockRegex.ReplaceAllStringFunc(html, func(codeBlock string) string {
+		// Extract the code content from the code block
+		matches := codeBlockRegex.FindStringSubmatch(codeBlock)
+		lang, codeContent := matches[1], matches[2]
+
+		codeContent = stripBlockTags(codeContent)
+
+		// Highlight the code content
+		highlightedCode := highlightSyntaxViaContent(codeContent, lang)
+
+		// Replace the original code block with the highlighted version
+		highlightedCodeBlock := "
" + highlightedCode + "
" + + return highlightedCodeBlock + }) + + return highlightedHTML +} diff --git a/src/utils/theme.go b/src/utils/theme.go new file mode 100644 index 0000000..b78d9c4 --- /dev/null +++ b/src/utils/theme.go @@ -0,0 +1,11 @@ +package utils + +import "os" + +func GetThemeFromEnv() string { + theme := os.Getenv("THEME") + if theme == "" { + theme = "auto" + } + return theme +} diff --git a/templates/home.html b/templates/home.html index 2189aca..490039f 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,81 +1,62 @@ - - AnonymousOverflow - Private frontend for StackOverflow - - - - - {{ template "sharedHead.html" }} - - -
-
- -

AnonymousOverflow

-
-

Get programming help without compromising your privacy.

-

- AnonymousOverflow allows you to view StackOverflow threads - without the cluttered interface and exposing your IP address, - browsing habits and other browser fingerprint data to - StackOverflow. -

- {{ if .successMessage }} -
-

Success: {{ .successMessage }}

-
- {{ else}} {{ if .errorMessage }} -
-

Error: {{ .errorMessage }}

-
- {{end}} {{ end }} -
-
- - -
-
-
-
- - Toggle theme - -
- {{ template "themeSwitcher.html" .}} -
- + + + AnonymousOverflow - Private frontend for StackOverflow + + + + + + {{ template "sharedHead.html" }} + + + +
+
+ +

Anonymous­Overflow

- +

Get programming help without compromising your privacy.

+

+ AnonymousOverflow allows you to view StackOverflow threads + without the cluttered interface and exposing your IP address, + browsing habits and other browser fingerprint data to + StackOverflow. +

+ {{ if .successMessage }} +
+

Success: {{ .successMessage }}

+
+ {{ else}} {{ if .errorMessage }} +
+

Error: {{ .errorMessage }}

+
+ {{end}} {{ end }} +
+
+ + +
+
+
+
+ + Toggle images + +
+
+ +
+ + diff --git a/templates/question.html b/templates/question.html index fa575e9..54e8893 100644 --- a/templates/question.html +++ b/templates/question.html @@ -1,111 +1,95 @@ - - {{ .question.Title }} | AnonymousOverflow - - - - - - {{ template "sharedHead.html" }} - - - + + {{ .question.Title }} | AnonymousOverflow + + + + + + {{ template "sharedHead.html" }} + - - - + + + + + + - -
-
- - - - {{ template "themeSwitcher.html" . }} -
-
-
-

{{ .question.Title }}

-

- Asked {{ .question.Timestamp }} by - {{ .question.AuthorName }}. -

-
-
{{ .question.Body }}
-
- {{ range .question.Tags }} -
{{ . }}
- {{ end }} -
- {{ if .question.Comments }} {{ template "comments.html" - .question }} {{end}} -
-
-
-

Answers

-
-
- - -
-
-
- {{ range $answer := .answers }} -
-
-

- {{ if $answer.IsAccepted }} Accepted - {{ end }} - {{$answer.Upvotes}} Votes -

- -
- Paperclip icon -
-
-
- {{ $answer.Body }} -
-
- Answered {{ $answer.Timestamp }} by - {{ $answer.AuthorName }} -
-
- {{ if $answer.Comments }} {{ template "comments.html" $answer }} - {{end}} -
+ + +
+ + + +
+
+
+

{{ .question.Title }}

+

+ Asked {{ .question.Timestamp }} by + {{ .question.AuthorName + }}. +

+
+
{{ .question.Body }}
+
+ {{ range .question.Tags }} +
{{ . }}
{{ end }}
- + {{ if .question.Comments }} {{ template "comments.html" + .question }} {{end}} +
+ {{ if .answers }} +
+
+

Answers

+
+
+ + +
+
+
+ {{ range $answer := .answers }} +
+
+

+ {{ if $answer.IsAccepted }} Accepted - {{ end }} + {{$answer.Upvotes}} Votes +

+ +
+ Paperclip icon +
+
+
+ {{ $answer.Body }} +
+
+ Answered {{ $answer.Timestamp }} by + {{ $answer.AuthorName }} +
+
+ {{ if $answer.Comments }} {{ template "comments.html" $answer }} + {{end}} +
+ {{ end }} + {{ end }} + + diff --git a/templates/themeSwitcher.html b/templates/themeSwitcher.html deleted file mode 100644 index a34b512..0000000 --- a/templates/themeSwitcher.html +++ /dev/null @@ -1,6 +0,0 @@ -