Compare commits
263 Commits
lambda
...
feature/us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5438365e50 | ||
|
|
3d707c561e | ||
|
|
9fd38bed03 | ||
|
|
877ee7faa9 | ||
|
|
d490581c44 | ||
|
|
730aead3a1 | ||
|
|
2024f7f6cf | ||
|
|
34ecc2a32b | ||
|
|
e5e1c38058 | ||
|
|
493a17385a | ||
|
|
a8abb43f3a | ||
|
|
d69d8dba0e | ||
|
|
e67da2c241 | ||
|
|
8de23e6b22 | ||
|
|
97ba8e827d | ||
|
|
2f80594106 | ||
|
|
f29df4fbd9 | ||
|
|
928f5145a4 | ||
|
|
34da56c001 | ||
|
|
498e9282e9 | ||
|
|
043150d85b | ||
|
|
f85a581486 | ||
|
|
85035243e9 | ||
|
|
73170689af | ||
|
|
e673ae279f | ||
|
|
e840649481 | ||
|
|
8dba4b7ca0 | ||
|
|
891a952e9b | ||
|
|
4ea167fde0 | ||
|
|
51f9bf4750 | ||
|
|
6af714f92f | ||
|
|
ed21e8293e | ||
|
|
d5e635c545 | ||
|
|
c163ae690e | ||
|
|
ea74f69c45 | ||
|
|
64908fb75d | ||
|
|
70c6103e24 | ||
|
|
23435cbe61 | ||
|
|
b65ee39162 | ||
|
|
fe639ab7ef | ||
|
|
7c78e156ba | ||
|
|
6e891c70d8 | ||
|
|
4de1fa1e79 | ||
|
|
922480f05a | ||
|
|
a16a302d5e | ||
|
|
f0188614b0 | ||
|
|
311bf9ef25 | ||
|
|
9525106ccc | ||
|
|
5e7cf6ec52 | ||
|
|
2b7de852e6 | ||
|
|
b84b1b8371 | ||
|
|
73338df7c7 | ||
|
|
8d0c866417 | ||
|
|
054a717570 | ||
|
|
6539c4ed30 | ||
|
|
b9dd049a83 | ||
|
|
9b5af0aeb6 | ||
|
|
4e065bf455 | ||
|
|
ffbea7868d | ||
|
|
30f5fdcbf2 | ||
|
|
39c670c81f | ||
|
|
2c88b519c9 | ||
|
|
c6a9ca3443 | ||
|
|
4977c5cbf4 | ||
|
|
75034fb99b | ||
|
|
81b63f6dea | ||
|
|
9dd1878421 | ||
|
|
0bef79a2a4 | ||
|
|
88f6e5b3b5 | ||
|
|
11418c96f5 | ||
|
|
77b2a2ef91 | ||
|
|
a3feae6b30 | ||
|
|
e03abd0ab9 | ||
|
|
dece361d98 | ||
|
|
841e02c0a9 | ||
|
|
55e2b77e3d | ||
|
|
99385f9ef4 | ||
|
|
2e3cd312bc | ||
|
|
62910defcf | ||
|
|
f89306e26a | ||
|
|
e82b47a729 | ||
|
|
82c360baa0 | ||
|
|
af4c674955 | ||
|
|
36a8e99c9a | ||
|
|
2d711df823 | ||
|
|
bc0c2d8008 | ||
|
|
5c2962d34d | ||
|
|
10289cde51 | ||
|
|
b8c2f453e5 | ||
|
|
459c23013f | ||
|
|
2d25b12aec | ||
|
|
6aba8e7183 | ||
|
|
594ed97746 | ||
|
|
9c7464cd08 | ||
|
|
681baf4d54 | ||
|
|
e58cfa872e | ||
|
|
69e7baa594 | ||
|
|
166d1e68f6 | ||
|
|
4cf3861387 | ||
|
|
9d65fa95b4 | ||
|
|
1f1d930072 | ||
|
|
ac1a4056c8 | ||
|
|
b8469ada4e | ||
|
|
50d71ad321 | ||
|
|
7cad41e11d | ||
|
|
a328b15698 | ||
|
|
d30ac03554 | ||
|
|
e73bd2e87b | ||
|
|
d6ebc9a54a | ||
|
|
46465e1e17 | ||
|
|
2a5e17628c | ||
|
|
23499a9710 | ||
|
|
b6d400688f | ||
|
|
b6adca7627 | ||
|
|
740ae0d925 | ||
|
|
cdbd3944d9 | ||
|
|
c93d32a043 | ||
|
|
8576b5d517 | ||
|
|
8a899bd85a | ||
|
|
4306b3bb81 | ||
|
|
f1c058cbfc | ||
|
|
b8d54dede6 | ||
|
|
0b25e637cc | ||
|
|
ced4d817c0 | ||
|
|
5944b7253d | ||
|
|
1263e66f45 | ||
|
|
f0c1b7f4fe | ||
|
|
432137d5bc | ||
|
|
d282f57a1c | ||
|
|
f9adc8adb0 | ||
|
|
d3799c6f7b | ||
|
|
3431c5c56b | ||
|
|
3db725cc5f | ||
|
|
3a5368fc1f | ||
|
|
523d347629 | ||
|
|
e477c1dabc | ||
|
|
37259cd1fd | ||
|
|
09945fabbc | ||
|
|
3f0382342c | ||
|
|
944e7ec387 | ||
|
|
58bf8469d9 | ||
|
|
bf0ae28599 | ||
|
|
ba796cbf09 | ||
|
|
351dc1b863 | ||
|
|
7477f97be4 | ||
|
|
ac228e8d1e | ||
|
|
5412b58be5 | ||
|
|
51fbb8d0a7 | ||
|
|
59a148e4f2 | ||
|
|
acf677db89 | ||
|
|
d50cf2da86 | ||
|
|
4da9b74949 | ||
|
|
ca6c50b513 | ||
|
|
150df2435d | ||
|
|
e0efe6caca | ||
|
|
da42f7cf90 | ||
|
|
27736300f8 | ||
|
|
416a34c731 | ||
|
|
5a4a55fef0 | ||
|
|
6b4e6e6436 | ||
|
|
be349ec9d9 | ||
|
|
95eaf959b0 | ||
|
|
3f29e71876 | ||
|
|
18c2c88d9f | ||
|
|
0d1bf4a008 | ||
|
|
4643f6d416 | ||
|
|
d299219533 | ||
|
|
d1a6768cef | ||
|
|
4bdcf9036d | ||
|
|
1bed1d4147 | ||
|
|
9dbb846ef5 | ||
|
|
b081604e00 | ||
|
|
1b8ab7b2a9 | ||
|
|
da44d02601 | ||
|
|
72366d47bd | ||
|
|
d664fcd169 | ||
|
|
5b6b8e8b63 | ||
|
|
5447d184d1 | ||
|
|
e022a451af | ||
|
|
965f5817c5 | ||
|
|
2ac2c64d9c | ||
|
|
a4f687da7b | ||
|
|
136631b7b6 | ||
|
|
6c95c2f16f | ||
|
|
ceabddc972 | ||
|
|
de49f1c647 | ||
|
|
b323b26b57 | ||
|
|
f3ac0369b1 | ||
|
|
b7d8f02542 | ||
|
|
465f36a59e | ||
|
|
92f80392d4 | ||
|
|
3383c24e4f | ||
|
|
ba2079449c | ||
|
|
b5081f2c4d | ||
|
|
dea4a3e5f1 | ||
|
|
0ed12fe16d | ||
|
|
73b3391763 | ||
|
|
2c695d4c61 | ||
|
|
c0f0d71d01 | ||
|
|
730ae1fa10 | ||
|
|
d05f7f868d | ||
|
|
6eee86d682 | ||
|
|
5569feb2c8 | ||
|
|
1b562dc6e7 | ||
|
|
eb27abbaae | ||
|
|
500ce9ceba | ||
|
|
c86cdb63d3 | ||
|
|
cdf35c1b53 | ||
|
|
d0904407af | ||
|
|
7b946e55b6 | ||
|
|
088f46a5bb | ||
|
|
4a41e59d34 | ||
|
|
6c49dee732 | ||
|
|
12684be6aa | ||
|
|
94b74675b1 | ||
|
|
72f6c56f62 | ||
|
|
43deb1087c | ||
|
|
1f25128b8e | ||
|
|
f75846b42e | ||
|
|
0749462457 | ||
|
|
afb1dd28a2 | ||
|
|
abcb3218dd | ||
|
|
1bb3353f70 | ||
|
|
5522ec8e99 | ||
|
|
7a0248f5aa | ||
|
|
ff8017d9b1 | ||
|
|
d96e0768bc | ||
|
|
a13a064547 | ||
|
|
f236963444 | ||
|
|
b03264b143 | ||
|
|
11a70b6e76 | ||
|
|
5f8bc2b7b8 | ||
|
|
038842a36b | ||
|
|
90ab3b37d3 | ||
|
|
db2195d17f | ||
|
|
34e779595d | ||
|
|
c10076d36b | ||
|
|
4572499658 | ||
|
|
156452713b | ||
|
|
718a017ccf | ||
|
|
d9b8b660f4 | ||
|
|
36793004f8 | ||
|
|
003b61a69d | ||
|
|
cf5a1ac18b | ||
|
|
07a202da8f | ||
|
|
5ba3b386a6 | ||
|
|
ece82ce9ab | ||
|
|
7f4d090943 | ||
|
|
601172a987 | ||
|
|
d90bf77e57 | ||
|
|
a029f0d197 | ||
|
|
379a1f3b81 | ||
|
|
9f746c8dbf | ||
|
|
11e8a00c1d | ||
|
|
5a015c48f1 | ||
|
|
86afc22616 | ||
|
|
4ea95ff592 | ||
|
|
220ecd847a | ||
|
|
740f62abd9 | ||
|
|
6e5b0ee127 | ||
|
|
41012edf9e | ||
|
|
5596998511 | ||
|
|
1dec0d4ca2 |
42
.air.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
|
||||
|
||||
# Working directory
|
||||
# . or absolute path, please note that the directories following must be under root.
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
full_bin = "ENV=dev go run main.go"
|
||||
# Watch these filename extensions.
|
||||
include_ext = ["go", "hbs"]
|
||||
# Ignore these filename extensions or directories.
|
||||
exclude_dir = ["tmp", "vendor", "static"]
|
||||
# Watch these directories if you specified.
|
||||
include_dir = []
|
||||
# Exclude files.
|
||||
exclude_file = []
|
||||
# This log file places in your tmp_dir.
|
||||
log = "air.log"
|
||||
# It's not necessary to trigger build each time file changes if it's too frequent.
|
||||
delay = 1000 # ms
|
||||
# Stop running old binary when build errors occur.
|
||||
stop_on_error = true
|
||||
# Send Interrupt signal before killing process (windows does not support this feature)
|
||||
send_interrupt = false
|
||||
# Delay after sending Interrupt signal
|
||||
kill_delay = 500 # ms
|
||||
|
||||
[log]
|
||||
# Show log time
|
||||
time = false
|
||||
|
||||
[color]
|
||||
# Customize each part's color. If no color found, use the raw app log.
|
||||
main = "magenta"
|
||||
watcher = "cyan"
|
||||
build = "yellow"
|
||||
runner = "green"
|
||||
|
||||
[misc]
|
||||
# Delete tmp directory on exit
|
||||
clean_on_exit = true
|
||||
@@ -1,3 +1,3 @@
|
||||
node_modules
|
||||
samples
|
||||
dist
|
||||
.env
|
||||
tmp
|
||||
static/app.css
|
||||
35
.env.example
Normal file
@@ -0,0 +1,35 @@
|
||||
ADDRESS=0.0.0.0
|
||||
PORT=3000
|
||||
FIBER_PREFORK=false
|
||||
IMGUR_CLIENT_ID=546c25a59c58ad7
|
||||
|
||||
# Instance privacy
|
||||
# For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy
|
||||
# Required to be on the instance list.
|
||||
|
||||
# Link to a privacy policy (optional)
|
||||
PRIVACY_POLICY=
|
||||
# Explain how this data is used/why it is collected (optional)
|
||||
PRIVACY_MESSAGE=
|
||||
# Country where instance is located. Leave blank if running on Tor without clearnet.
|
||||
PRIVACY_COUNTRY=
|
||||
# Hosting provider or ISP name. Leave blank if running on Tor without clearnet.
|
||||
PRIVACY_PROVIDER=
|
||||
# Set to true if you use Cloudflare (using Cloudflare only as DNS (gray cloud icon), set to false)
|
||||
PRIVACY_CLOUDFLARE=false
|
||||
|
||||
# These settings are for NGINX without data collection disabled.
|
||||
# Instructions for other reverse proxies and to disable data collection:
|
||||
# https://codeberg.org/librarian/librarian/wiki/Instance-privacy
|
||||
|
||||
# Set to true if no data is collected. (overrides all other options)
|
||||
PRIVACY_NOT_COLLECTED=false
|
||||
# IP address
|
||||
PRIVACY_IP=true
|
||||
# Request URL
|
||||
PRIVACY_URL=true
|
||||
# Device Type (User agent)
|
||||
PRIVACY_DEVICE=true
|
||||
# If the data collection is only collected when there is an error and only stored for
|
||||
# a short amount of time while diagnosing an issue.
|
||||
PRIVACY_DIAGNOSTICS=false
|
||||
6
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
dist
|
||||
node_modules
|
||||
.env
|
||||
tmp
|
||||
static/app.css
|
||||
dist/
|
||||
|
||||
52
.goreleaser.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
before:
|
||||
hooks:
|
||||
- tailwindcss -i static/tailwind.css -o static/app.css -m
|
||||
- go mod tidy
|
||||
project_name: rimgo
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -X codeberg.org/rimgo/rimgo/pages.VersionInfo={{.Version}}
|
||||
archives:
|
||||
- format: tar.gz
|
||||
name_template: >-
|
||||
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
kos:
|
||||
- repository: codeberg.org/rimgo/rimgo
|
||||
tags:
|
||||
- '{{.Version}}'
|
||||
- latest
|
||||
bare: true
|
||||
preserve_import_paths: false
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
sbom: none
|
||||
gitea_urls:
|
||||
api: https://codeberg.org/api/v1
|
||||
download: https://codeberg.org
|
||||
release:
|
||||
gitea:
|
||||
owner: rimgo
|
||||
name: rimgo
|
||||
name_template: "{{.ProjectName}} v{{.Version}}"
|
||||
disable: false
|
||||
mode: append
|
||||
|
||||
# The lines beneath this are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
33
Dockerfile
@@ -1,14 +1,21 @@
|
||||
FROM alpine as build
|
||||
# install build tools
|
||||
RUN apk add go git
|
||||
RUN go env -w GOPROXY=direct
|
||||
# cache dependencies
|
||||
ADD go.mod go.sum ./
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /src
|
||||
RUN apk --no-cache add ca-certificates git nodejs npm
|
||||
COPY . .
|
||||
|
||||
RUN npx tailwindcss -i static/tailwind.css -o static/app.css -m
|
||||
RUN go mod download
|
||||
# build
|
||||
ADD . .
|
||||
RUN go build -o /main
|
||||
# copy artifacts to a clean image
|
||||
FROM alpine
|
||||
COPY --from=build /main /main
|
||||
ENTRYPOINT [ "/main" ]
|
||||
RUN GOOS=linux GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-X codeberg.org/rimgo/rimgo/pages.VersionInfo=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)"
|
||||
|
||||
FROM scratch as bin
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /src/rimgo .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["/app/rimgo"]
|
||||
|
||||
7
Justfile
Normal file
@@ -0,0 +1,7 @@
|
||||
build:
|
||||
tailwindcss -i static/tailwind.css -o static/app.css -m
|
||||
CGO_ENABLED=0 go build -o rimgo -ldflags "-X codeberg.org/rimgo/rimgo/pages.VersionInfo=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)"
|
||||
|
||||
dev:
|
||||
tailwindcss -i static/tailwind.css -o static/app.css -m -w &
|
||||
go run github.com/cosmtrek/air@latest -c .air.toml
|
||||
195
README.md
@@ -1,116 +1,133 @@
|
||||
<img src="https://codeberg.org/video-prize-ranch/rimgo/raw/branch/main/static/img/rimgo.svg" width="96" height="96" />
|
||||
<img alt="" src="https://codeberg.org/rimgo/rimgo/raw/branch/main/static/img/rimgo.svg" width="96" height="96" />
|
||||
|
||||
# rimgo
|
||||
An alternative frontend for Imgur. Based on [rimgu](https://codeberg.org/3np/rimgu) and rewritten in Go.
|
||||
An alternative frontend for Imgur. Originally based on [rimgu](https://codeberg.org/3np/rimgu).
|
||||
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">
|
||||
<img alt="License: AGPLv3" src="https://shields.io/badge/License-AGPL%20v3-blue.svg">
|
||||
<img alt="License: AGPLv3" src="https://shields.io/badge/License-AGPL%20v3-blue.svg" height="20px">
|
||||
</a>
|
||||
<a href="https://matrix.to/#/#rimgo:nitro.chat">
|
||||
<img alt="Matrix" src="https://img.shields.io/badge/chat-matrix-blue" height="20px">
|
||||
</a>
|
||||
|
||||
It's read-only and works without JavaScript. Images and albums can be viewed without wasting resources from downloading and running tracking scripts. No sign-up nags.
|
||||
## Table of Contents
|
||||
- [Features](#features)
|
||||
- [Comparison](#comparison)
|
||||
- [Speed](#speed)
|
||||
- [Privacy](#privacy)
|
||||
- [Usage](#usage)
|
||||
- [Instances](#instances)
|
||||
- [Clearnet](#clearnet)
|
||||
- [Tor](#tor)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
### Documentation
|
||||
|
||||
Our new documentation is now available at [https://rimgo.codeberg.page/docs/](https://rimgo.codeberg.page/docs/)!
|
||||
|
||||
- [Install](https://rimgo.codeberg.page/docs/getting-started/install/)
|
||||
- [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/)
|
||||
- [Redirection](https://rimgo.codeberg.page/docs/usage/configuration/)
|
||||
- [Instance privacy](https://rimgo.codeberg.page/docs/usage/instance-privacy/)
|
||||
|
||||
## Features
|
||||
- Lightweight
|
||||
- No JavaScript
|
||||
- No ads or tracking
|
||||
- No sign up or app install prompts
|
||||
- Bandwidth efficient - automatically uses newer image formats (if enabled)
|
||||
|
||||
- [x] URL-compatible with i.imgur.com - just replace the domain in the URL
|
||||
- [x] Images and videos (gifv, mp4)
|
||||
- [ ] Galleries with comments
|
||||
- [x] Albums
|
||||
- [ ] User page
|
||||
- [ ] Tag page
|
||||
## Comparison
|
||||
Comparing rimgo to Imgur.
|
||||
|
||||
This is currently very early stage software. Some things left to implement (contributions welcome!):
|
||||
### Speed
|
||||
Tested using [Google PageSpeed Insights](https://pagespeed.web.dev/).
|
||||
|
||||
- [x] Streaming (currently media is downloaded in full in rimgu before it's returned)
|
||||
- [ ] Localization/internationalization
|
||||
- [x] Pretty CSS styling (responsive?)
|
||||
- [ ] Support for other popular image sites
|
||||
- [ ] Filtering and exploration on user/tags pages
|
||||
- [ ] Responsive scaling of videos on user/tags pages
|
||||
- [x] Logo
|
||||
| | [rimgo](https://pagespeed.web.dev/report?url=https%3A%2F%2Fi.bcow.xyz%2Fgallery%2FgYiQLWy) | [Imgur](https://pagespeed.web.dev/report?url=https%3A%2F%2Fimgur.com%2Fgallery%2FgYiQLWy) |
|
||||
| ------------------- | ------- | --------- |
|
||||
| Performance | 91 | 28 |
|
||||
| Request count | 29 | 340 |
|
||||
| Resource Size | 218 KiB | 2,542 KiB |
|
||||
| Time to Interactive | 1.6s | 23.8s |
|
||||
|
||||
Things that are considered out of scope:
|
||||
### Privacy
|
||||
Imgur collects information about your device and uses tracking cookies for advertising, this is mentioned in their [privacy policy](https://imgur.com/privacy/). [Blacklight](https://themarkup.org/blacklight) found 31 trackers and 87 third-party cookies.
|
||||
|
||||
* Uploading, commenting, voting, etc - rimgo is read-only.
|
||||
* Authentication, serving HTTPS, rate limiting, etc - Use a reverse proxy or load balancer like Caddy, Traefik, or NGINX.
|
||||
* Anything requiring JavaScript or the client directly interacting with upstream servers.
|
||||
See what cookies and trackers Imgur uses and where your data gets sent: https://themarkup.org/blacklight?url=imgur.com
|
||||
|
||||
## Usage
|
||||
Replace imgur.com or i.imgur.com with the instance domain. For i.stack.imgur.com, replace i.stack.imgur.com with the instance domain and add stack/ before the media ID. You can use a browser extension to do this [automatically](#automatically-redirect-links).
|
||||
|
||||
Imgur: `https://imgur.com/gallery/j2sOQkJ` -> `https://rimgo.bcow.xyz/gallery/j2sOQkJ`
|
||||
Stack Overflow: `https://i.stack.imgur.com/KnO3v.jpg?s=64&g=1` -> `https://rimgo.bcow.xyz/stack/KnO3v.jpg?s=64&g=1`
|
||||
|
||||
To automatically redirect Imgur links, see [Redirection](https://rimgo.codeberg.page/docs/usage/redirection/).
|
||||
|
||||
## Instances
|
||||
Open an issue to have your instance listed here! See the rules for the instance list [here](https://rimgo.codeberg.page/docs/usage/instance-list-rules/).
|
||||
|
||||
Open an issue to have your instance listed here!
|
||||
> For more details on instance privacy, see https://rimgo.codeberg.page/docs/usage/instance-privacy/
|
||||
|
||||
| Website | Country | Cloudflare |
|
||||
|------------------------------------------------------------------------------------------------------------------------------------------|---------|------------|
|
||||
| [i.bcow.xyz](https://i.bcow.xyz/) (official) | 🇨🇦 CA | |
|
||||
| [rimgo.pussthecat.org](https://rimgo.pussthecat.org/) | 🇩🇪 DE | |
|
||||
| [img.riverside.rocks](https://img.riverside.rocks) | 🇺🇸 US | |
|
||||
| [rimgo.totaldarkness.net](https://rimgo.totaldarkness.net/) | 🇨🇦 CA | |
|
||||
| [rimgo.bus-hit.me](https://rimgo.bus-hit.me/) | 🇨🇦 CA | |
|
||||
| [l4d4owboqr6xcmd6lf64gbegel62kbudu3x3jnldz2mx6mhn3bsv3zyd.onion](http://l4d4owboqr6xcmd6lf64gbegel62kbudu3x3jnldz2mx6mhn3bsv3zyd.onion/) | | |
|
||||
| [jx3dpcwedpzu2mh54obk5gvl64i2ln7pt5mrzd75s4jnndkqwzaim7ad.onion](http://jx3dpcwedpzu2mh54obk5gvl64i2ln7pt5mrzd75s4jnndkqwzaim7ad.onion) | 🇺🇸 US | |
|
||||
### Clearnet
|
||||
|
||||
| URL | Country | Provider | Privacy | Notes |
|
||||
| :------------------------------------------------------------ | :----------- | :----------------------- | :-------------------- | :---- |
|
||||
| [rimgo.pussthecat.org](https://rimgo.pussthecat.org) | 🇩🇪 DE | Hetzner | ⚠️ Data collected | |
|
||||
| [rimgo.totaldarkness.net](https://rimgo.totaldarkness.net) | 🇨🇦 CA | Vultr | ✅ Data not collected | |
|
||||
| [rimgo.bus-hit.me](https://rimgo.bus-hit.me) | 🇨🇦 CA | Oracle | ⚠️ Data collected | |
|
||||
| [imgur.artemislena.eu](https://imgur.artemislena.eu) | 🇩🇪 DE | Vodafone Deutschland | ✅ Data not collected | Self-hosted, provider is ISP |
|
||||
| [rimgo.vern.cc](https://rimgo.vern.cc) | 🇺🇸 US | OVHCloud | ✅ Data not collected | [Edited theme](https://git.vern.cc/root/modifications/src/branch/master/rimgo) |
|
||||
| [rim.odyssey346.dev](https://rim.odyssey346.dev/) | 🇫🇷️ FR | Trolling Solutions (OVH) | ✅ Data not collected | |
|
||||
| [i.habedieeh.re](https://i.habedieeh.re/) | 🇨🇦️ CA | Oracle Cloud | ✅ Data not collected | |
|
||||
| [rimgo.hostux.net](https://rimgo.hostux.net/) | 🇫🇷️ FR | Gandi | ⚠️ Data collected | |
|
||||
| [ri.zzls.xyz](https://ri.zzls.xyz/) | 🇨🇱 CL | TELEFÓNICA CHILE | ✅ Data not collected | Self-hosted, provider is ISP |
|
||||
| [rimgo.lunar.icu](https://rimgo.marcopisco.com/) | 🇩🇪 DE | Cloudflare | ✅ Data not collected | |
|
||||
| [imgur.010032.xyz](https://imgur.010032.xyz/) | 🇰🇷 KR | Oracle Cloud | ✅ Data not collected | |
|
||||
| [rimgo.kling.gg](https://rimgo.kling.gg/) | 🇳🇱 NL | RamNode | ✅ Data not collected | |
|
||||
| [i.01r.xyz](https://i.01r.xyz/) | 🇺🇸 US | Cloudflare | ✅ Data not collected | |
|
||||
| [rimgo.projectsegfau.lt](https://rimgo.projectsegfau.lt/) | 🇫🇷 FR, 🇺🇸 US, 🇮🇳 IN | See below | ✅ Data not collected | |
|
||||
| [rimgo.eu.projectsegfau.lt](https://rimgo.eu.projectsegfau.lt/) | 🇫🇷 FR | Orange S.A. | ✅ Data not collected | |
|
||||
| [rimgo.us.projectsegfau.lt](https://rimgo.us.projectsegfau.lt/) | 🇺🇸 US | Racknerd | ✅ Data not collected | |
|
||||
| [rimgo.in.projectsegfau.lt](https://rimgo.in.projectsegfau.lt/) | 🇮🇳 IN | Airtel | ✅ Data not collected | |
|
||||
| [rimgo.whateveritworks.org](https://rimgo.whateveritworks.org/) | 🇩🇪 DE | Cloudflare | ✅ Data not collected | |
|
||||
| [rimgo.nohost.network](https://rimgo.nohost.network/) | 🇲🇽 MX | Telmex | ✅ Data not collected | |
|
||||
| [rimgo.catsarch.com](https://rimgo.catsarch.com/) | 🇺🇸 US | Comcast | ✅ Data not collected | Self-hosted, provider is ISP |
|
||||
| [rimgo.frontendfriendly.xyz](https://rimgo.frontendfriendly.xyz/) | 🇩🇪 DE | Hetzner | ⚠️ Data collected | |
|
||||
|
||||
### Tor
|
||||
|
||||
| URL | Privacy | Notes |
|
||||
| :-- | :------ | :----------------------- |
|
||||
| [rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | ✅ Data not collected | Onion of rimgo.vern.cc |
|
||||
| [imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion](http://imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion) | ✅ Data not collected | Onion of imgur.artemislena.eu |
|
||||
| [rim.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd.onion](http://rim.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd.onion) | ⚠️ Data collected | |
|
||||
| [tdp6uqjtmok723suum5ms3jbquht6d7dssug4cgcxhfniatb25gcipad.onion](http://tdp6uqjtmok723suum5ms3jbquht6d7dssug4cgcxhfniatb25gcipad.onion) | ✅ Data not collected | Onion of rimgo.privacytools.io |
|
||||
| [i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion](http://i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion/) | ✅ Data not collected | Onion of i.habedieeh.re |
|
||||
| [rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion](http://rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion/) | ✅ Data not collected | Onion of ri.zzls.xyz |
|
||||
| [tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion/](http://tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion/) | ✅ Data not collected | Onion of rimgo.kling.gg |
|
||||
| [rimgo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion](http://rimgo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/) | ✅ Data not collected | Onion of rimgo.eu.projectsegfau.lt |
|
||||
|
||||
### I2P
|
||||
|
||||
| URL | Privacy | Notes |
|
||||
| :-- | :------ | :----------------------- |
|
||||
| [rimgo.i2p](http://rimgo.i2p) | ✅ Data not collected | i.habedieeh.re on I2P |
|
||||
| [rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p](http://rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p) | ✅ Data not collected | Same as rimgo.i2p |
|
||||
| [rimgo.zzls.i2p](http://rimgo.zzls.i2p) | ✅ Data not collected | ri.zzls.xyz on I2P |
|
||||
| [p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p](http://p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p) | ✅ Data not collected | Same as rimgo.zzls.i2p |
|
||||
| [ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p](http://ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p) | ✅ Data not collected | rimgo.kling.gg on I2P |
|
||||
|
||||
## Install
|
||||
rimgo can run on any platform Go compiles on.
|
||||
|
||||
### Docker
|
||||
Install Docker and docker-compose, then clone this repository.
|
||||
```
|
||||
git clone https://codeberg.org/video-prize-ranch/rimgo
|
||||
cd rimgo
|
||||
```
|
||||
|
||||
Edit the `docker-compose.yml` file using your favorite text editor.
|
||||
```
|
||||
nvim docker-compose.yml
|
||||
```
|
||||
|
||||
You can now run rimgo.
|
||||
```
|
||||
sudo docker-compose up -d
|
||||
```
|
||||
|
||||
### Build from source
|
||||
|
||||
#### Requirements
|
||||
* Go v1.16 or later
|
||||
|
||||
Clone the repository and `cd` into it.
|
||||
```
|
||||
git clone https://codeberg.org/video-prize-ranch/rimgo
|
||||
cd rimgo
|
||||
```
|
||||
|
||||
Build rimgo.
|
||||
```
|
||||
go build
|
||||
```
|
||||
|
||||
Edit the config file using your preferred editor.
|
||||
```
|
||||
nvim config.yml
|
||||
```
|
||||
|
||||
You can now run rimgo.
|
||||
```
|
||||
./rimgo
|
||||
```
|
||||
|
||||
See [Install](https://rimgo.codeberg.page/docs/getting-started/install/).
|
||||
|
||||
## Configuration
|
||||
|
||||
rimgo can be configured using environment variables or a config file.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Name | Default |
|
||||
|-----------------------|-----------------|
|
||||
| RIMGU_PORT | 3000 |
|
||||
| RIMGU_HOST | localhost |
|
||||
| RIMGU_ADDRESS | 0.0.0.0 |
|
||||
| RIMGU_IMGUR_CLIENT_ID | 546c25a59c58ad7 |
|
||||
See [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/).
|
||||
|
||||
## Contributing
|
||||
Pull requests are welcome! If you have any questions or bug reports, open an [issue](https://codeberg.org/rimgo/rimgo/issues/new).
|
||||
|
||||
PRs are welcome!
|
||||
|
||||
This software is released under the AGPL 3.0 license. In short, this means that if you make any modifications to the code and then publish the result (e.g. by hosting the result on a web server), you must publicly distribute your changes and declare that they also use AGPL 3.0.
|
||||
## License
|
||||
This software is released under the AGPL-3.0 license. If you make any modifications to the code and distribute it (including use on a network server), you must publicly distribute your changes and release them under the AGPL-3.0.
|
||||
|
||||
143
api/album.go
@@ -1,75 +1,117 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/types"
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"github.com/spf13/viper"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func FetchAlbum(albumID string) (types.Album, error) {
|
||||
// https://api.imgur.com/post/v1/albums/zk7mdKH?client_id=${CLIENT_ID}&include=media%2Caccount
|
||||
type Album struct {
|
||||
Id string
|
||||
Title string
|
||||
Views int64
|
||||
Upvotes int64
|
||||
Downvotes int64
|
||||
SharedWithCommunity bool
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
Comments int64
|
||||
User User
|
||||
Media []Media
|
||||
Tags []Tag
|
||||
}
|
||||
|
||||
res, err := http.Get("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID") + "&include=media%2Caccount")
|
||||
type Media struct {
|
||||
Id string
|
||||
Name string
|
||||
Title string
|
||||
Description string
|
||||
Url string
|
||||
Type string
|
||||
MimeType string
|
||||
}
|
||||
|
||||
func (client *Client) FetchAlbum(albumID string) (Album, error) {
|
||||
cacheData, found := client.Cache.Get(albumID + "-album")
|
||||
if found {
|
||||
return cacheData.(Album), nil
|
||||
}
|
||||
|
||||
data, err := utils.GetJSON("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount")
|
||||
if err != nil {
|
||||
return types.Album{}, err
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
album, err := parseAlbum(data)
|
||||
if err != nil {
|
||||
return types.Album{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
album := types.Album{}
|
||||
if data.Get("shared_with_community").Bool() {
|
||||
album, err = FetchPosts(albumID)
|
||||
} else {
|
||||
album, err = ParseAlbum(data)
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
client.Cache.Set(albumID+"-album", album, 1*time.Hour)
|
||||
return album, err
|
||||
}
|
||||
|
||||
func FetchPosts(albumID string) (types.Album, error) {
|
||||
res, err := http.Get("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID") + "&include=media%2Caccount")
|
||||
func (client *Client) FetchPosts(albumID string) (Album, error) {
|
||||
cacheData, found := client.Cache.Get(albumID + "-posts")
|
||||
if found {
|
||||
return cacheData.(Album), nil
|
||||
}
|
||||
|
||||
data, err := utils.GetJSON("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount%2Ctags")
|
||||
if err != nil {
|
||||
return types.Album{}, err
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
album, err := parseAlbum(data)
|
||||
if err != nil {
|
||||
return types.Album{}, err
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
return ParseAlbum(data)
|
||||
client.Cache.Set(albumID+"-posts", album, 1*time.Hour)
|
||||
return album, nil
|
||||
}
|
||||
|
||||
func ParseAlbum(data gjson.Result) (types.Album, error) {
|
||||
media := make([]types.Media, 0)
|
||||
func (client *Client) FetchMedia(mediaID string) (Album, error) {
|
||||
cacheData, found := client.Cache.Get(mediaID + "-media")
|
||||
if found {
|
||||
return cacheData.(Album), nil
|
||||
}
|
||||
|
||||
data, err := utils.GetJSON("https://api.imgur.com/post/v1/media/" + mediaID + "?client_id=" + client.ClientID + "&include=media%2Caccount")
|
||||
if err != nil {
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
album, err := parseAlbum(data)
|
||||
if err != nil {
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
client.Cache.Set(mediaID+"-media", album, 1*time.Hour)
|
||||
return album, nil
|
||||
}
|
||||
|
||||
func parseAlbum(data gjson.Result) (Album, error) {
|
||||
media := make([]Media, 0)
|
||||
data.Get("media").ForEach(
|
||||
func(key gjson.Result, value gjson.Result) bool {
|
||||
url := value.Get("url").String()
|
||||
url = strings.ReplaceAll(url, "https://i.imgur.com", "")
|
||||
|
||||
if strings.HasSuffix(url, "mp4") || viper.GetBool("CF_ALL_MEDIA") {
|
||||
url = viper.GetString("CF_MEDIA_DISTRIBUTION") + url
|
||||
}
|
||||
description := value.Get("metadata.description").String()
|
||||
description = strings.ReplaceAll(description, "\n", "<br>")
|
||||
description = bluemonday.UGCPolicy().Sanitize(description)
|
||||
|
||||
media = append(media, types.Media{
|
||||
media = append(media, Media{
|
||||
Id: value.Get("id").String(),
|
||||
Name: value.Get("name").String(),
|
||||
MimeType: value.Get("mime_type").String(),
|
||||
Type: value.Get("type").String(),
|
||||
Title: value.Get("metadata.title").String(),
|
||||
Description: value.Get("metadata.description").String(),
|
||||
Description: description,
|
||||
Url: url,
|
||||
})
|
||||
|
||||
@@ -77,12 +119,25 @@ func ParseAlbum(data gjson.Result) (types.Album, error) {
|
||||
},
|
||||
)
|
||||
|
||||
tags := make([]Tag, 0)
|
||||
data.Get("tags").ForEach(
|
||||
func(key gjson.Result, value gjson.Result) bool {
|
||||
tags = append(tags, Tag{
|
||||
Tag: value.Get("tag").String(),
|
||||
Display: value.Get("display").String(),
|
||||
Background: "/" + value.Get("background_id").String() + ".webp",
|
||||
BackgroundId: value.Get("background_id").String(),
|
||||
})
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
createdAt, err := utils.FormatDate(data.Get("created_at").String())
|
||||
if err != nil {
|
||||
return types.Album{}, err
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
return types.Album{
|
||||
album := Album{
|
||||
Id: data.Get("id").String(),
|
||||
Title: data.Get("title").String(),
|
||||
SharedWithCommunity: data.Get("shared_with_community").Bool(),
|
||||
@@ -92,5 +147,17 @@ func ParseAlbum(data gjson.Result) (types.Album, error) {
|
||||
Comments: data.Get("comment_count").Int(),
|
||||
CreatedAt: createdAt,
|
||||
Media: media,
|
||||
}, nil
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
account := data.Get("account")
|
||||
if account.Raw != "" {
|
||||
album.User = User{
|
||||
Id: account.Get("id").Int(),
|
||||
Username: account.Get("username").String(),
|
||||
Avatar: strings.ReplaceAll(account.Get("avatar_url").String(), "https://i.imgur.com", ""),
|
||||
}
|
||||
}
|
||||
|
||||
return album, nil
|
||||
}
|
||||
|
||||
21
api/client.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ClientID string
|
||||
Cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewClient(clientId string) (*Client) {
|
||||
client := Client{
|
||||
ClientID: clientId,
|
||||
Cache: cache.New(15*time.Minute, 15*time.Minute),
|
||||
}
|
||||
|
||||
return &client
|
||||
}
|
||||
102
api/comments.go
@@ -1,43 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/types"
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/tidwall/gjson"
|
||||
"gitlab.com/golang-commonmark/linkify"
|
||||
)
|
||||
|
||||
func FetchComments(galleryID string) ([]types.Comment, error) {
|
||||
// https://api.imgur.com/comment/v1/comments?client_id=546c25a59c58ad7&filter[post]=eq:g1bk7CB&include=account&per_page=30&sort=best
|
||||
|
||||
res, err := http.Get("https://api.imgur.com/comment/v1/comments?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID") + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
|
||||
if err != nil {
|
||||
return []types.Comment{}, err
|
||||
type Comment struct {
|
||||
Comments []Comment
|
||||
User User
|
||||
Post Submission
|
||||
Id string
|
||||
Comment string
|
||||
Upvotes int64
|
||||
Downvotes int64
|
||||
Platform string
|
||||
CreatedAt string
|
||||
RelTime string
|
||||
UpdatedAt string
|
||||
DeletedAt string
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return []types.Comment{}, err
|
||||
func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
|
||||
cacheData, found := client.Cache.Get(galleryID + "-comments")
|
||||
if found {
|
||||
return cacheData.([]Comment), nil
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
data, err := utils.GetJSON("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
|
||||
if err != nil {
|
||||
return []Comment{}, nil
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
comments := make([]types.Comment, 0)
|
||||
comments := make([]Comment, 0)
|
||||
data.Get("data").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
comments = append(comments, ParseComment(value))
|
||||
comments = append(comments, parseComment(value))
|
||||
}()
|
||||
|
||||
return true
|
||||
@@ -45,29 +56,34 @@ func FetchComments(galleryID string) ([]types.Comment, error) {
|
||||
)
|
||||
wg.Wait()
|
||||
|
||||
client.Cache.Set(galleryID+"-comments", comments, cache.DefaultExpiration)
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
func ParseComment(data gjson.Result) types.Comment {
|
||||
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
|
||||
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
|
||||
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
|
||||
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
|
||||
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
|
||||
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
|
||||
|
||||
func parseComment(data gjson.Result) Comment {
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05Z", data.Get("created_at").String())
|
||||
createdAt := createdTime.Format("January 2, 2006 3:04 PM")
|
||||
updatedAt, _ := utils.FormatDate(data.Get("updated_at").String())
|
||||
deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String())
|
||||
|
||||
userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "")
|
||||
if viper.GetBool("CF_ALL_MEDIA") {
|
||||
userAvatar = viper.GetString("CF_MEDIA_DISTRIBUTION") + userAvatar
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
comments := make([]types.Comment, 0)
|
||||
comments := make([]Comment, 0)
|
||||
data.Get("comments").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
comments = append(comments, ParseComment(value))
|
||||
comments = append(comments, parseComment(value))
|
||||
}()
|
||||
|
||||
return true
|
||||
@@ -75,15 +91,49 @@ func ParseComment(data gjson.Result) types.Comment {
|
||||
)
|
||||
wg.Wait()
|
||||
|
||||
return types.Comment{
|
||||
comment := data.Get("comment").String()
|
||||
|
||||
comment = strings.ReplaceAll(comment, "\n", "<br>")
|
||||
|
||||
for _, match := range imgRe.FindAllString(comment, -1) {
|
||||
img := iImgurRe.ReplaceAllString(match, "")
|
||||
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
|
||||
comment = strings.Replace(comment, match, img, 1)
|
||||
}
|
||||
for _, match := range vidRe.FindAllString(comment, -1) {
|
||||
vid := iImgurRe.ReplaceAllString(match, "")
|
||||
vid = `<video class="comment__media" controls loop preload="none" poster="` + vidFormatRe.ReplaceAllString(vid, ".webp") + `"><source type="` + strings.Split(vid, ".")[1] + `" src="` + vid + `" /></video>`
|
||||
comment = strings.Replace(comment, match, vid, 1)
|
||||
}
|
||||
for _, l := range linkify.Links(comment) {
|
||||
origLink := comment[l.Start:l.End]
|
||||
link := `<a href="` + origLink + `">` + origLink + `</a>`
|
||||
comment = strings.Replace(comment, origLink, link, 1)
|
||||
}
|
||||
comment = imgurRe.ReplaceAllString(comment, "/$1/$2")
|
||||
comment = imgurRe2.ReplaceAllString(comment, "/$1")
|
||||
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowImages()
|
||||
p.AllowElements("video", "source")
|
||||
p.AllowAttrs("src", "tvpe").OnElements("source")
|
||||
p.AllowAttrs("controls", "loop", "preload", "poster").OnElements("video")
|
||||
p.AllowAttrs("class", "loading").OnElements("img", "video")
|
||||
p.RequireNoReferrerOnLinks(true)
|
||||
p.RequireNoFollowOnLinks(true)
|
||||
p.RequireCrossOriginAnonymous(true)
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
return Comment{
|
||||
Comments: comments,
|
||||
User: types.User{
|
||||
User: User{
|
||||
Id: data.Get("account.id").Int(),
|
||||
Username: data.Get("account.username").String(),
|
||||
Avatar: userAvatar,
|
||||
},
|
||||
Post: parseSubmission(data.Get("post")),
|
||||
Id: data.Get("id").String(),
|
||||
Comment: data.Get("comment").String(),
|
||||
Comment: comment,
|
||||
Upvotes: data.Get("upvote_count").Int(),
|
||||
Downvotes: data.Get("downvote_count").Int(),
|
||||
Platform: data.Get("platform").String(),
|
||||
|
||||
19
api/f.ts
@@ -1,19 +0,0 @@
|
||||
export const fetchUserPosts = async (userID: string, sort: Sorting = 'newest'): Promise<Post[]> => {
|
||||
/* eslint-disable max-len */
|
||||
// https://api.imgur.com/3/account/mombotnumber5/submissions/0/newest?album_previews=1&client_id=${CLIENT_ID}
|
||||
const response = await get(
|
||||
``,
|
||||
);
|
||||
return JSON.parse(response.body).data;
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
|
||||
export const fetchTagPosts = async (tagID: string, sort: Sorting = 'viral'): Promise<TagResult> => {
|
||||
/* eslint-disable max-len */
|
||||
// https://api.imgur.com/3/account/mombotnumber5/submissions/0/newest?album_previews=1&client_id=${CLIENT_ID}
|
||||
const response = await get(
|
||||
`https://api.imgur.com/3/gallery/t/${tagID.toLowerCase()}/${sort}/week/0?client_id=${CONFIG.imgur_client_id}`,
|
||||
);
|
||||
return JSON.parse(response.body).data;
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
72
api/search.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
Id string
|
||||
Url string
|
||||
ImageUrl string
|
||||
Title string
|
||||
User string
|
||||
Points string
|
||||
Views string
|
||||
RelTime string
|
||||
}
|
||||
|
||||
func (client *Client) Search(query string, page string) ([]SearchResult, error) {
|
||||
query = url.QueryEscape(query)
|
||||
req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/" + page + "?scrolled&q_size_is_mpx=off&qs=list&q=" + query, nil)
|
||||
if err != nil {
|
||||
return []SearchResult{}, err
|
||||
}
|
||||
utils.SetReqHeaders(req)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return []SearchResult{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
if err != nil {
|
||||
return []SearchResult{}, err
|
||||
}
|
||||
|
||||
results := []SearchResult{}
|
||||
doc.Find(".post-list").Each(func(i int, s *goquery.Selection) {
|
||||
url, _ := s.Find("a").Attr("href")
|
||||
imageUrl, _ := s.Find("img").Attr("src")
|
||||
|
||||
postInfo := strings.Split(s.Find(".post-info").Text(), "·")
|
||||
points := strings.TrimSpace(postInfo[0])
|
||||
points = strings.TrimSuffix(points, " points")
|
||||
views := strings.TrimSpace(postInfo[1])
|
||||
views = strings.TrimSuffix(views, " views")
|
||||
|
||||
result := SearchResult{
|
||||
Id: strings.Split(url, "/")[2],
|
||||
Url: url,
|
||||
ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""),
|
||||
Title: s.Find(".search-item-title a").Text(),
|
||||
User: s.Find(".account").Text(),
|
||||
Views: views,
|
||||
Points: points,
|
||||
RelTime: strings.TrimSpace(postInfo[2]),
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
110
api/tag.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Tag string
|
||||
Display string
|
||||
Sort string
|
||||
PostCount int64
|
||||
Posts []Submission
|
||||
Background string
|
||||
BackgroundId string
|
||||
}
|
||||
|
||||
func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error) {
|
||||
cacheData, found := client.Cache.Get(tag + sort + page + "-tag")
|
||||
if found {
|
||||
return cacheData.(Tag), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://api.imgur.com/post/v1/posts/t/"+tag, nil)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("client_id", client.ClientID)
|
||||
q.Add("include", "cover")
|
||||
q.Add("page", page)
|
||||
|
||||
switch sort {
|
||||
case "newest":
|
||||
q.Add("filter[window]", "week")
|
||||
q.Add("sort", "-time")
|
||||
case "best":
|
||||
q.Add("filter[window]", "all")
|
||||
q.Add("sort", "-top")
|
||||
case "popular":
|
||||
default:
|
||||
q.Add("filter[window]", "week")
|
||||
q.Add("sort", "-viral")
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
posts := make([]Submission, 0)
|
||||
data.Get("posts").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
posts = append(posts, Submission{
|
||||
Id: value.Get("id").String(),
|
||||
Title: value.Get("title").String(),
|
||||
Link: strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""),
|
||||
Cover: Media{
|
||||
Id: value.Get("cover_id").String(),
|
||||
Type: value.Get("cover.type").String(),
|
||||
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
|
||||
},
|
||||
Points: value.Get("point_count").Int(),
|
||||
Upvotes: value.Get("upvote_count").Int(),
|
||||
Downvotes: value.Get("downvote_count").Int(),
|
||||
Comments: value.Get("comment_count").Int(),
|
||||
Views: value.Get("view_count").Int(),
|
||||
IsAlbum: value.Get("is_album").Bool(),
|
||||
})
|
||||
}()
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
tagData := Tag{
|
||||
Tag: tag,
|
||||
Display: data.Get("display").String(),
|
||||
Sort: sort,
|
||||
PostCount: data.Get("post_count").Int(),
|
||||
Posts: posts,
|
||||
Background: "/" + data.Get("background_id").String() + ".webp",
|
||||
}
|
||||
|
||||
client.Cache.Set(tag + sort + page + "-tag", tagData, cache.DefaultExpiration)
|
||||
return tagData, nil
|
||||
}
|
||||
107
api/trending.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func (client *Client) FetchTrending(section, sort, page string) ([]Submission, error) {
|
||||
cacheData, found := client.Cache.Get(fmt.Sprintf("trending-%s-%s-%s", section, sort, page))
|
||||
if found {
|
||||
return cacheData.([]Submission), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://api.imgur.com/post/v1/posts", nil)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
utils.SetReqHeaders(req)
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("client_id", client.ClientID)
|
||||
q.Add("include", "cover")
|
||||
q.Add("page", page)
|
||||
|
||||
switch sort {
|
||||
case "newest":
|
||||
q.Add("filter[window]", "week")
|
||||
q.Add("sort", "-time")
|
||||
case "best":
|
||||
q.Add("filter[window]", "all")
|
||||
q.Add("sort", "-top")
|
||||
case "popular":
|
||||
fallthrough
|
||||
default:
|
||||
q.Add("filter[window]", "week")
|
||||
q.Add("sort", "-viral")
|
||||
sort = "popular"
|
||||
}
|
||||
switch section {
|
||||
case "hot":
|
||||
q.Add("filter[section]", "eq:hot")
|
||||
case "new":
|
||||
q.Add("filter[section]", "eq:new")
|
||||
case "top":
|
||||
q.Add("filter[section]", "eq:top")
|
||||
q.Add("filter[window]", "day")
|
||||
default:
|
||||
q.Add("filter[section]", "eq:hot")
|
||||
section = "hot"
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
posts := make([]Submission, 0)
|
||||
data.ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
posts = append(posts, Submission{
|
||||
Id: value.Get("id").String(),
|
||||
Title: value.Get("title").String(),
|
||||
Link: strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""),
|
||||
Cover: Media{
|
||||
Id: value.Get("cover_id").String(),
|
||||
Type: value.Get("cover.type").String(),
|
||||
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
|
||||
},
|
||||
Points: value.Get("point_count").Int(),
|
||||
Upvotes: value.Get("upvote_count").Int(),
|
||||
Downvotes: value.Get("downvote_count").Int(),
|
||||
Comments: value.Get("comment_count").Int(),
|
||||
Views: value.Get("view_count").Int(),
|
||||
IsAlbum: value.Get("is_album").Bool(),
|
||||
})
|
||||
}()
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
client.Cache.Set(fmt.Sprintf("trending-%s-%s-%s", section, sort, page), posts, cache.DefaultExpiration)
|
||||
return posts, nil
|
||||
}
|
||||
272
api/user.go
@@ -1,62 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/types"
|
||||
"github.com/spf13/viper"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func FetchUser(username string) (types.User, error) {
|
||||
res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID"))
|
||||
type User struct {
|
||||
Id int64
|
||||
Bio string
|
||||
Username string
|
||||
Points int64
|
||||
Cover string
|
||||
Avatar string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
type Submission struct {
|
||||
Id string
|
||||
Title string
|
||||
Link string
|
||||
Cover Media
|
||||
Points int64
|
||||
Upvotes int64
|
||||
Downvotes int64
|
||||
Comments int64
|
||||
Views int64
|
||||
IsAlbum bool
|
||||
}
|
||||
|
||||
func (client *Client) FetchUser(username string) (User, error) {
|
||||
cacheData, found := client.Cache.Get(username + "-user")
|
||||
if found {
|
||||
return cacheData.(User), nil
|
||||
}
|
||||
|
||||
res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + utils.Config.ImgurId)
|
||||
if err != nil {
|
||||
return types.User{}, err
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return types.User{}, err
|
||||
}
|
||||
|
||||
var user types.User
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
return types.User{}, err
|
||||
}
|
||||
|
||||
user.Cover = strings.ReplaceAll(user.Cover, "https://imgur.com", "")
|
||||
user.Avatar = strings.ReplaceAll(user.Avatar, "https://i.imgur.com", "")
|
||||
|
||||
if viper.GetBool("CF_ALL_MEDIA") {
|
||||
user.Avatar = viper.GetString("CF_MEDIA_DISTRIBUTION") + user.Avatar
|
||||
}
|
||||
|
||||
createdTime, _ := time.Parse("2006-01-02T15:04:05Z", user.CreatedAt)
|
||||
user.CreatedAt = createdTime.Format("January 2, 2006")
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func FetchSubmissions(username string, sort string, page string) ([]types.Submission, error) {
|
||||
res, err := http.Get("https://api.imgur.com/3/account/" + username + "/submissions/" + page + "/" + sort + "?album_previews=1&client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID"))
|
||||
if err != nil {
|
||||
return []types.Submission{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return []types.Submission{}, err
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
submissions := []types.Submission{}
|
||||
createdTime, _ := time.Parse(time.RFC3339, data.Get("created_at").String())
|
||||
|
||||
user := User{
|
||||
Id: data.Get("id").Int(),
|
||||
Bio: data.Get("bio").String(),
|
||||
Username: data.Get("username").String(),
|
||||
Points: data.Get("reputation_count").Int(),
|
||||
Cover: strings.ReplaceAll(data.Get("cover_url").String(), "https://imgur.com", ""),
|
||||
Avatar: strings.ReplaceAll(data.Get("avatar_url").String(), "https://i.imgur.com", ""),
|
||||
CreatedAt: createdTime.Format("January 2, 2006"),
|
||||
}
|
||||
|
||||
client.Cache.Set(username+"-user", user, 1*time.Hour)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (client *Client) FetchSubmissions(username string, sort string, page string) ([]Submission, error) {
|
||||
cacheData, found := client.Cache.Get(username + "-submissions-" + sort + page)
|
||||
if found {
|
||||
return cacheData.([]Submission), nil
|
||||
}
|
||||
|
||||
data, err := utils.GetJSON("https://api.imgur.com/3/account/" + username + "/submissions/" + page + "/" + sort + "?album_previews=1&client_id=" + utils.Config.ImgurId)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
|
||||
submissions := []Submission{}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
data.Get("data").ForEach(
|
||||
@@ -66,35 +90,159 @@ func FetchSubmissions(username string, sort string, page string) ([]types.Submis
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
cover := value.Get("images.#(id==\"" + value.Get("cover").String() + "\")")
|
||||
|
||||
url := strings.ReplaceAll(cover.Get("link").String(), "https://i.imgur.com", "")
|
||||
if strings.HasSuffix(url, "mp4") || viper.GetBool("CF_ALL_MEDIA") {
|
||||
url = viper.GetString("CF_MEDIA_DISTRIBUTION") + url
|
||||
}
|
||||
|
||||
submissions = append(submissions, types.Submission{
|
||||
Id: value.Get("id").String(),
|
||||
Link: strings.ReplaceAll(value.Get("link").String(), "https://imgur.com", ""),
|
||||
Title: value.Get("title").String(),
|
||||
Cover: types.Media{
|
||||
Id: cover.Get("id").String(),
|
||||
Description: cover.Get("description").String(),
|
||||
Type: strings.Split(cover.Get("type").String(), "/")[0],
|
||||
Url: url,
|
||||
},
|
||||
Points: cover.Get("points").Int(),
|
||||
Upvotes: cover.Get("ups").Int(),
|
||||
Downvotes: cover.Get("downs").Int(),
|
||||
Comments: cover.Get("comment_count").Int(),
|
||||
Views: cover.Get("views").Int(),
|
||||
IsAlbum: cover.Get("is_album").Bool(),
|
||||
})
|
||||
submissions = append(submissions, parseSubmission(value))
|
||||
}()
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
wg.Wait()
|
||||
|
||||
client.Cache.Set(username+"-submissions-"+sort+page, submissions, 15*time.Minute)
|
||||
return submissions, nil
|
||||
}
|
||||
|
||||
func (client *Client) FetchUserFavorites(username string, sort string, page string) ([]Submission, error) {
|
||||
cacheData, found := client.Cache.Get(username + "-favorites-" + sort + page)
|
||||
if found {
|
||||
return cacheData.([]Submission), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://api.imgur.com/3/account/"+username+"/gallery_favorites/"+page+"/"+sort, nil)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
utils.SetReqHeaders(req)
|
||||
q := req.URL.Query()
|
||||
q.Add("client_id", client.ClientID)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return []Submission{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
submissions := []Submission{}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
data.Get("data").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
submissions = append(submissions, parseSubmission(value))
|
||||
}()
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
wg.Wait()
|
||||
|
||||
client.Cache.Set(username+"-favorites-"+sort+page, submissions, 15*time.Minute)
|
||||
return submissions, nil
|
||||
}
|
||||
|
||||
func (client *Client) FetchUserComments(username string) ([]Comment, error) {
|
||||
cacheData, found := client.Cache.Get(username + "-usercomments")
|
||||
if found {
|
||||
return cacheData.([]Comment), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://api.imgur.com/comment/v1/comments", nil)
|
||||
if err != nil {
|
||||
return []Comment{}, err
|
||||
}
|
||||
utils.SetReqHeaders(req)
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("client_id", client.ClientID)
|
||||
q.Add("filter[account]", "eq:"+username)
|
||||
q.Add("include", "account,post")
|
||||
q.Add("sort", "new")
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return []Comment{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return []Comment{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
comments := make([]Comment, 0)
|
||||
data.Get("data").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
comments = append(comments, parseComment(value))
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
client.Cache.Set(username+"-usercomments", comments, cache.DefaultExpiration)
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
func parseSubmission(value gjson.Result) Submission {
|
||||
var cover Media
|
||||
c := value.Get("cover")
|
||||
coverData := value.Get("images.#(id==\"" + c.String() + "\")")
|
||||
switch {
|
||||
case c.Type == gjson.String && coverData.Exists():
|
||||
cover = Media{
|
||||
Id: coverData.Get("id").String(),
|
||||
Description: coverData.Get("description").String(),
|
||||
Type: strings.Split(coverData.Get("type").String(), "/")[0],
|
||||
Url: strings.ReplaceAll(coverData.Get("link").String(), "https://i.imgur.com", ""),
|
||||
}
|
||||
// This case is when fetching comments
|
||||
case c.Type != gjson.Null:
|
||||
cover = Media{
|
||||
Id: c.Get("id").String(),
|
||||
Url: strings.ReplaceAll(c.Get("url").String(), "https://i.imgur.com", ""),
|
||||
}
|
||||
// Replace with thumbnails here because it's easier.
|
||||
if strings.HasSuffix(cover.Url, ".mp4") {
|
||||
cover.Url = cover.Url[:len(cover.Url)-3] + "webp"
|
||||
}
|
||||
default:
|
||||
cover = Media{
|
||||
Id: value.Get("id").String(),
|
||||
Description: value.Get("description").String(),
|
||||
Type: strings.Split(value.Get("type").String(), "/")[0],
|
||||
Url: strings.ReplaceAll(value.Get("link").String(), "https://i.imgur.com", ""),
|
||||
}
|
||||
}
|
||||
|
||||
id := value.Get("id").String()
|
||||
|
||||
link := "/a/" + id
|
||||
if value.Get("in_gallery").Bool() {
|
||||
link = "/gallery/" + id
|
||||
}
|
||||
|
||||
return Submission{
|
||||
Id: id,
|
||||
Link: link,
|
||||
Title: value.Get("title").String(),
|
||||
Cover: cover,
|
||||
Points: value.Get("points").Int(),
|
||||
Upvotes: value.Get("ups").Int(),
|
||||
Downvotes: value.Get("downs").Int(),
|
||||
Comments: value.Get("comment_count").Int(),
|
||||
Views: value.Get("views").Int(),
|
||||
IsAlbum: value.Get("is_album").Bool(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
RIMGU_PORT: 3000
|
||||
RIMGU_HOST: localhost
|
||||
RIMGU_ADDRESS: 0.0.0.0
|
||||
RIMGU_IMGUR_CLIENT_ID: 546c25a59c58ad7
|
||||
@@ -2,10 +2,16 @@ version: '3'
|
||||
|
||||
services:
|
||||
rimgo:
|
||||
#image: quay.io/pussthecatorg/rimgo # Uncomment to use image
|
||||
build: .
|
||||
image: codeberg.org/rimgo/rimgo # Official image
|
||||
#image: quay.io/pussthecatorg/rimgo # Unofficial image
|
||||
#build: . # Uncomment to build from source
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ./config.yml:/app/config.yml
|
||||
restart: unless-stopped
|
||||
user: 65534:65534 # equivalent to `nobody`
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges
|
||||
cap_drop:
|
||||
- ALL
|
||||
env_file: .env
|
||||
57
go.mod
@@ -1,38 +1,41 @@
|
||||
module codeberg.org/video-prize-ranch/rimgo
|
||||
module codeberg.org/rimgo/rimgo
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/aws/aws-lambda-go v1.28.0
|
||||
github.com/awslabs/aws-lambda-go-api-proxy v0.12.0
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/gofiber/fiber/v2 v2.24.0
|
||||
github.com/gofiber/template v1.6.21
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/tidwall/gjson v1.12.1
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/aymerick/raymond v2.0.2+incompatible
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gofiber/fiber/v2 v2.48.0
|
||||
github.com/gofiber/template/handlebars/v2 v2.1.4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.2 // indirect
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.4 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/gofiber/template v1.8.2 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.31.0 // indirect
|
||||
github.com/valyala/fasthttp v1.48.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
)
|
||||
|
||||
156
instances.json
Normal file
@@ -0,0 +1,156 @@
|
||||
[
|
||||
{
|
||||
"url": "https://rimgo.pussthecat.org",
|
||||
"countries": [
|
||||
"de"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.totaldarkness.net",
|
||||
"countries": [
|
||||
"ca"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.bus-hit.me",
|
||||
"countries": [
|
||||
"ca"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://imgur.artemislena.eu",
|
||||
"onion": "http://imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion",
|
||||
"countries": [
|
||||
"de"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.vern.cc",
|
||||
"onion": "http://rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion",
|
||||
"countries": [
|
||||
"ca"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rim.odyssey346.dev",
|
||||
"countries": [
|
||||
"fr"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://imgur.010032.xyz",
|
||||
"countries": [
|
||||
"kr"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://i.habedieeh.re",
|
||||
"onion": "http://i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion",
|
||||
"i2p": [
|
||||
"rimgo.i2p",
|
||||
"rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p"
|
||||
],
|
||||
"countries": [
|
||||
"ca"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.hostux.net",
|
||||
"countries": [
|
||||
"fr"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://ri.zzls.xyz",
|
||||
"onion": "http://rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion",
|
||||
"i2p": [
|
||||
"rimgo.zzls.i2p",
|
||||
"p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p"
|
||||
],
|
||||
"countries": [
|
||||
"cl"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.lunar.icu",
|
||||
"countries": [
|
||||
"de"
|
||||
],
|
||||
"cloudflare": true
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.kling.gg",
|
||||
"onion": "http://tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion",
|
||||
"i2p": "http://ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p",
|
||||
"countries": [
|
||||
"nl"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://i.01r.xyz",
|
||||
"countries": [
|
||||
"us"
|
||||
],
|
||||
"cloudflare": true
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.eu.projectsegfau.lt",
|
||||
"countries": [
|
||||
"fr"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.us.projectsegfau.lt",
|
||||
"countries": [
|
||||
"us"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.in.projectsegfau.lt",
|
||||
"countries": [
|
||||
"in"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.whateveritworks.org",
|
||||
"countries": [
|
||||
"de"
|
||||
],
|
||||
"cloudflare": true
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.nohost.network",
|
||||
"countries": [
|
||||
"mx"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.catsarch.com",
|
||||
"countries": [
|
||||
"us"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.frontendfriendly.xyz",
|
||||
"countries": [
|
||||
"de"
|
||||
],
|
||||
"cloudflare": false
|
||||
}
|
||||
]
|
||||
152
main.go
@@ -1,71 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/pages"
|
||||
"codeberg.org/video-prize-ranch/rimgo/static"
|
||||
"codeberg.org/video-prize-ranch/rimgo/views"
|
||||
"github.com/aws/aws-lambda-go/events"
|
||||
"github.com/aws/aws-lambda-go/lambda"
|
||||
fiberadaptor "github.com/awslabs/aws-lambda-go-api-proxy/fiber"
|
||||
"codeberg.org/rimgo/rimgo/pages"
|
||||
"codeberg.org/rimgo/rimgo/static"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/views"
|
||||
"github.com/aymerick/raymond"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/template/handlebars"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/template/handlebars/v2"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var fiberLambda *fiberadaptor.FiberLambda
|
||||
func main() {
|
||||
envPath := flag.String("c", ".env", "Path to env file")
|
||||
godotenv.Load(*envPath)
|
||||
utils.LoadConfig()
|
||||
|
||||
func init() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yml")
|
||||
viper.AddConfigPath("/etc/rimgu/")
|
||||
viper.AddConfigPath("$HOME/.config/rimgu")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AutomaticEnv()
|
||||
pages.InitializeApiClient()
|
||||
|
||||
viper.SetDefault("RIMGU_PORT", "3000")
|
||||
viper.SetDefault("RIMGU_HOST", "localhost")
|
||||
viper.SetDefault("RIMGU_ADDRESS", "0.0.0.0")
|
||||
viper.SetDefault("RIMGU_IMGUR_CLIENT_ID", "546c25a59c58ad7")
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
views := http.FS(views.GetFiles())
|
||||
if os.Getenv("ENV") == "dev" {
|
||||
views = http.Dir("./views")
|
||||
}
|
||||
engine := handlebars.NewFileSystem(views, ".hbs")
|
||||
|
||||
engine.AddFunc("noteq", func(a interface{}, b interface{}, options *raymond.Options) interface{} {
|
||||
if raymond.Str(a) != raymond.Str(b) {
|
||||
return options.Fn()
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
engine := handlebars.NewFileSystem(http.FS(views.GetFiles()), ".hbs")
|
||||
app := fiber.New(fiber.Config{
|
||||
Views: engine,
|
||||
Prefork: viper.GetBool("FIBER_PREFORK"),
|
||||
Prefork: utils.Config.FiberPrefork,
|
||||
UnescapePath: true,
|
||||
StreamRequestBody: false,
|
||||
StreamRequestBody: true,
|
||||
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
|
||||
code := fiber.StatusInternalServerError
|
||||
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
err = ctx.Status(code).Render("errors/error", fiber.Map{
|
||||
"err": err,
|
||||
})
|
||||
if err != nil {
|
||||
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
app.Use(recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
|
||||
fmt.Println(e)
|
||||
},
|
||||
}))
|
||||
|
||||
if os.Getenv("ENV") == "dev" {
|
||||
app.Use("/static", filesystem.New(filesystem.Config{
|
||||
Root: http.Dir("./static"),
|
||||
}))
|
||||
app.Get("/errors/429", func(c *fiber.Ctx) error {
|
||||
return c.Render("errors/429", nil)
|
||||
})
|
||||
app.Get("/errors/404", func(c *fiber.Ctx) error {
|
||||
return c.Render("errors/404", nil)
|
||||
})
|
||||
app.Get("/errors/error", func(c *fiber.Ctx) error {
|
||||
return c.Render("errors/error", fiber.Map{
|
||||
"err": "Test error",
|
||||
})
|
||||
})
|
||||
} else {
|
||||
app.Use("/static", filesystem.New(filesystem.Config{
|
||||
MaxAge: 2592000,
|
||||
Root: http.FS(static.GetFiles()),
|
||||
}))
|
||||
app.Use(cache.New(cache.Config{
|
||||
Expiration: 30 * time.Minute,
|
||||
MaxBytes: 25000000,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.OriginalURL()
|
||||
},
|
||||
CacheControl: true,
|
||||
StoreResponseHeaders: true,
|
||||
}))
|
||||
}
|
||||
|
||||
app.Get("/robots.txt", func(c *fiber.Ctx) error {
|
||||
file, _ := static.GetFiles().ReadFile("robots.txt")
|
||||
_, err := c.Write(file)
|
||||
return err
|
||||
})
|
||||
app.Get("/favicon.ico", func(c *fiber.Ctx) error {
|
||||
file, _ := static.GetFiles().ReadFile("favicon/favicon.ico")
|
||||
_, err := c.Write(file)
|
||||
return err
|
||||
})
|
||||
|
||||
app.Get("/", pages.FrontpageHandler)
|
||||
app.Get("/:baseName.:extension", pages.HandleMedia)
|
||||
app.Get("/a/:galleryID", pages.HandleGallery)
|
||||
//app.Get("/t/:tagID", pages.HandleAlbum)
|
||||
app.Get("/", pages.HandleFrontpage)
|
||||
app.Get("/about", pages.HandleAbout)
|
||||
app.Get("/privacy", pages.HandlePrivacy)
|
||||
app.Get("/search", pages.HandleSearch)
|
||||
app.Get("/trending", pages.HandleTrending)
|
||||
app.Get("/a/:postID", pages.HandlePost)
|
||||
app.Get("/a/:postID/embed", pages.HandleEmbed)
|
||||
app.Get("/t/:tag", pages.HandleTag)
|
||||
app.Get("/t/:tag/:postID", pages.HandlePost)
|
||||
app.Get("/r/:sub/:postID", pages.HandlePost)
|
||||
app.Get("/user/:userID", pages.HandleUser)
|
||||
app.Get("/user/:userID/favorites", pages.HandleUserFavorites)
|
||||
app.Get("/user/:userID/comments", pages.HandleUserComments)
|
||||
app.Get("/user/:userID/cover", pages.HandleUserCover)
|
||||
app.Get("/user/:userID/avatar", pages.HandleUserAvatar)
|
||||
app.Get("/gallery/:galleryID", pages.HandleGallery)
|
||||
app.Get("/gallery/:postID", pages.HandlePost)
|
||||
app.Get("/gallery/:postID/embed", pages.HandleEmbed)
|
||||
app.Get("/:postID.gifv", pages.HandleGifv)
|
||||
app.Get("/:baseName.:extension", pages.HandleMedia)
|
||||
app.Get("/stack/:baseName.:extension", pages.HandleMedia)
|
||||
app.Get("/:postID", pages.HandlePost)
|
||||
app.Get("/:postID/embed", pages.HandleEmbed)
|
||||
|
||||
fiberLambda = fiberadaptor.New(app)
|
||||
}
|
||||
|
||||
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
|
||||
return fiberLambda.ProxyWithContext(ctx, req)
|
||||
}
|
||||
|
||||
func main() {
|
||||
lambda.Start(Handler)
|
||||
app.Listen(utils.Config.Addr + ":" + utils.Config.Port)
|
||||
}
|
||||
22
pages/about.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
|
||||
func HandleAbout(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("about", fiber.Map{
|
||||
"proto": c.Protocol(),
|
||||
"domain": c.Hostname(),
|
||||
"force_webp": os.Getenv("FORCE_WEBP"),
|
||||
})
|
||||
}
|
||||
12
pages/apiClient.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
var ApiClient *api.Client
|
||||
|
||||
func InitializeApiClient() {
|
||||
ApiClient = api.NewClient(utils.Config.ImgurId)
|
||||
}
|
||||
48
pages/embed.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleEmbed(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
|
||||
|
||||
post, err := api.Album{}, error(nil)
|
||||
switch {
|
||||
case strings.HasPrefix(c.Path(), "/a"):
|
||||
post, err = ApiClient.FetchAlbum(c.Params("postID"))
|
||||
case strings.HasPrefix(c.Path(), "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(c.Params("postID"))
|
||||
default:
|
||||
post, err = ApiClient.FetchMedia(c.Params("postID"))
|
||||
}
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", nil)
|
||||
}
|
||||
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("embed", fiber.Map{
|
||||
"post": post,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleGifv(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("gifv", fiber.Map{
|
||||
"id": c.Params("postID"),
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func FrontpageHandler(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; style-src 'self' *.cloudfront.net; img-src 'self' *.cloudfront.net; font-src 'self' *.cloudfront.net; block-all-mixed-content")
|
||||
var VersionInfo string
|
||||
|
||||
return c.Render("frontpage", fiber.Map{})
|
||||
func HandleFrontpage(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("frontpage", fiber.Map{
|
||||
"config": utils.Config,
|
||||
"version": VersionInfo,
|
||||
})
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"codeberg.org/video-prize-ranch/rimgo/api"
|
||||
"codeberg.org/video-prize-ranch/rimgo/types"
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleGallery(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; style-src 'self' *.cloudfront.net; media-src 'self' *.cloudfront.net; img-src 'self' *.cloudfront.net; font-src 'self' *.cloudfront.net; block-all-mixed-content")
|
||||
|
||||
album, err := api.FetchAlbum(c.Params("galleryID"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comments := []types.Comment{}
|
||||
if album.SharedWithCommunity {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
comments, err = api.FetchComments(c.Params("galleryID"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
}
|
||||
|
||||
return c.Render("gallery", fiber.Map{
|
||||
"album": album,
|
||||
"comments": comments,
|
||||
})
|
||||
}
|
||||
14
pages/h.ts
@@ -1,14 +0,0 @@
|
||||
export const handleTag = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||
// https://imgur.com/t/funny
|
||||
if (!CONFIG.use_api) {
|
||||
return 'Tag page disabled. Rimgu administrator needs to enable API for this to work.';
|
||||
}
|
||||
const tagID = request.params.tagID;
|
||||
const result = await fetchTagPosts(tagID);
|
||||
return h.view('posts', {
|
||||
posts: result.items,
|
||||
pageTitle: CONFIG.page_title,
|
||||
tag: result,
|
||||
util,
|
||||
});
|
||||
};
|
||||
@@ -1,17 +1,22 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleMedia(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
if strings.HasPrefix(c.Path(), "/stack") {
|
||||
return handleMedia(c, "https://i.stack.imgur.com/" + strings.ReplaceAll(c.Params("baseName"), "stack/", "") + "." + c.Params("extension"))
|
||||
} else {
|
||||
return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUserCover(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
@@ -25,18 +30,50 @@ func HandleUserAvatar(c *fiber.Ctx) error {
|
||||
|
||||
func handleMedia(c *fiber.Ctx, url string) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; block-all-mixed-content")
|
||||
|
||||
res, err := http.Get(url)
|
||||
if os.Getenv("FORCE_WEBP") == "1" && c.Query("no_webp") == "" && c.Accepts("image/webp") == "image/webp" && !strings.HasPrefix(c.Path(), "/stack") {
|
||||
url = strings.ReplaceAll(url, ".png", ".webp")
|
||||
url = strings.ReplaceAll(url, ".jpg", ".webp")
|
||||
url = strings.ReplaceAll(url, ".jpeg", ".webp")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Path(), "/stack") && strings.Contains(c.OriginalURL(), "?") {
|
||||
url = url + "?" + strings.Split(c.OriginalURL(), "?")[1]
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
utils.SetReqHeaders(req)
|
||||
|
||||
if c.Get("Range") != "" {
|
||||
req.Header.Set("Range", c.Get("Range"))
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Status(res.StatusCode)
|
||||
if res.StatusCode == 404 {
|
||||
return c.Render("errors/404", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
} else if res.StatusCode == 429 {
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("Accept-Ranges", "bytes")
|
||||
c.Set("Content-Type", res.Header.Get("Content-Type"));
|
||||
return c.Send(body)
|
||||
c.Set("Content-Length", res.Header.Get("Content-Length"))
|
||||
if res.Header.Get("Content-Range") != "" {
|
||||
c.Set("Content-Range", res.Header.Get("Content-Range"))
|
||||
}
|
||||
|
||||
return c.SendStream(res.Body)
|
||||
}
|
||||
66
pages/post.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandlePost(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
|
||||
post, err := api.Album{}, error(nil)
|
||||
switch {
|
||||
case strings.HasPrefix(c.Path(), "/a"):
|
||||
post, err = ApiClient.FetchAlbum(c.Params("postID"))
|
||||
case strings.HasPrefix(c.Path(), "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(c.Params("postID"))
|
||||
case strings.HasPrefix(c.Path(), "/t"):
|
||||
post, err = ApiClient.FetchPosts(c.Params("postID"))
|
||||
default:
|
||||
post, err = ApiClient.FetchMedia(c.Params("postID"))
|
||||
}
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comments := []api.Comment{}
|
||||
if post.SharedWithCommunity {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
comments, err = ApiClient.FetchComments(c.Params("postID"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
}
|
||||
|
||||
nonce := ""
|
||||
csp := "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content; style-src 'self'"
|
||||
if len(post.Tags) != 0 {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
nonce = fmt.Sprintf("%x", b)
|
||||
csp = csp + " 'nonce-" + nonce + "'"
|
||||
}
|
||||
c.Set("Content-Security-Policy", csp)
|
||||
|
||||
return c.Render("post", fiber.Map{
|
||||
"post": post,
|
||||
"comments": comments,
|
||||
"nonce": nonce,
|
||||
})
|
||||
}
|
||||
17
pages/privacy.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandlePrivacy(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
|
||||
return c.Render("privacy", fiber.Map{
|
||||
"config": utils.Config,
|
||||
"version": VersionInfo,
|
||||
})
|
||||
}
|
||||
50
pages/search.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleSearch(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
query := c.Query("q")
|
||||
|
||||
if utils.ImgurRe.MatchString(query) {
|
||||
return c.Redirect(utils.ImgurRe.ReplaceAllString(query, ""))
|
||||
}
|
||||
|
||||
page := "0"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
displayPrevPage := true
|
||||
if page == "0" {
|
||||
displayPrevPage = false
|
||||
}
|
||||
|
||||
results, err := ApiClient.Search(query, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("search", fiber.Map{
|
||||
"query": query,
|
||||
"results": results,
|
||||
"page": pageNumber + 1,
|
||||
"displayPrev": displayPrevPage,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
51
pages/tag.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleTag(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "1"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
displayPrevPage := true
|
||||
if page == "1" {
|
||||
displayPrevPage = false
|
||||
}
|
||||
|
||||
tag, err := ApiClient.FetchTag(c.Params("tag"), c.Query("sort"), page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.Display == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
}
|
||||
|
||||
return c.Render("tag", fiber.Map{
|
||||
"tag": tag,
|
||||
"page": page,
|
||||
"displayPrev": displayPrevPage,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
58
pages/trending.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleTrending(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "1"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
if err != nil {
|
||||
pageNumber = 1
|
||||
}
|
||||
|
||||
section := c.Query("section")
|
||||
switch section {
|
||||
case "hot", "new", "top":
|
||||
default:
|
||||
section = "hot"
|
||||
}
|
||||
sort := c.Query("sort")
|
||||
switch sort {
|
||||
case "newest", "best", "popular":
|
||||
default:
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
displayPrevPage := true
|
||||
if page == "1" {
|
||||
displayPrevPage = false
|
||||
}
|
||||
|
||||
results, err := ApiClient.FetchTrending(section, sort, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("trending", fiber.Map{
|
||||
"results": results,
|
||||
"section": section,
|
||||
"sort": sort,
|
||||
"page": pageNumber,
|
||||
"displayPrev": displayPrevPage,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
144
pages/user.go
@@ -1,42 +1,142 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/api"
|
||||
"codeberg.org/video-prize-ranch/rimgo/types"
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleUser(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; style-src 'self' 'unsafe-inline' *.cloudfront.net; media-src 'self' *.cloudfront.net; img-src 'self' *.cloudfront.net; font-src 'self' *.cloudfront.net; block-all-mixed-content")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
user, err := types.User{}, error(nil)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
user, err = api.FetchUser(c.Params("userID"))
|
||||
}()
|
||||
page := "0"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
user, err := ApiClient.FetchUser(c.Params("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
}
|
||||
|
||||
submissions, err := ApiClient.FetchSubmissions(c.Params("userID"), "newest", page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
c.Status(429)
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
submissions, err := []types.Submission{}, error(nil)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
submissions, err = api.FetchSubmissions(c.Params("userID"), "newest", "0")
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return c.Render("user", fiber.Map{
|
||||
"user": user,
|
||||
"submissions": submissions,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUserComments(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
user, err := ApiClient.FetchUser(c.Params("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
}
|
||||
|
||||
comments, err := ApiClient.FetchUserComments(c.Params("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
c.Status(429)
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("userComments", fiber.Map{
|
||||
"user": user,
|
||||
"comments": comments,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUserFavorites(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "0"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
user, err := ApiClient.FetchUser(c.Params("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
}
|
||||
|
||||
favorites, err := ApiClient.FetchUserFavorites(c.Params("userID"), "newest", page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
c.Status(429)
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("userFavorites", fiber.Map{
|
||||
"user": user,
|
||||
"favorites": favorites,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
port: process.env.RIMGU_PORT || 8080,
|
||||
host: process.env.RIMGU_HOST || 'localhost',
|
||||
address: process.env.RIMGU_ADDRESS || '127.0.0.1',
|
||||
http_proxy: process.env.RIMGU_HTTP_PROXY || null,
|
||||
https_proxy: process.env.RIMGU_HTTPS_PROXY || null,
|
||||
imgur_client_id: process.env.RIMGU_IMGUR_CLIENT_ID || null,
|
||||
use_api: process.env.RIMGU_USE_API && process.env.RIMGU_USE_API !== 'false',
|
||||
page_title: process.env.RIMGU_PAGE_TITLE || 'rimgu',
|
||||
debug: process.env.RIMGU_DEBUG && process.env.RIMGU_DEBUG !== 'false',
|
||||
};
|
||||
143
src/types/index.d.ts
vendored
@@ -1,143 +0,0 @@
|
||||
interface Account {
|
||||
id: number;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
type MediaMimeType = 'image/jpeg' | 'image/png' | 'image/gif';
|
||||
type MediaType = 'image';
|
||||
type MediaExt = 'jpeg' | 'png' | 'gif';
|
||||
type Sorting = 'newest' | 'oldest' | 'best' | 'viral';
|
||||
|
||||
interface Tag {
|
||||
tag: string;
|
||||
display: string;
|
||||
background_id: string;
|
||||
accent: string;
|
||||
is_promoted: boolean;
|
||||
}
|
||||
|
||||
interface Media {
|
||||
id: string;
|
||||
account_id: number;
|
||||
mime_type: MediaMimeType;
|
||||
type: MediaType;
|
||||
name: string;
|
||||
basename: string;
|
||||
url: string;
|
||||
ext: MediaExt;
|
||||
width: number;
|
||||
height: number;
|
||||
size: number;
|
||||
metadata: {
|
||||
title: string;
|
||||
description: string;
|
||||
is_animated: boolean;
|
||||
is_looping: boolean;
|
||||
duration: number;
|
||||
has_sound: boolean;
|
||||
},
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
type MediaPlatform = 'ios' | 'android' | 'api' | 'web';
|
||||
interface Comment {
|
||||
id: number;
|
||||
parent_id: number;
|
||||
comment: string;
|
||||
account_id: number;
|
||||
post_id: string;
|
||||
upvote_count: number;
|
||||
downvote_count: number;
|
||||
point_count: number;
|
||||
vote: null; // ?
|
||||
platform_id: number;
|
||||
platform: MediaPlatform;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: null;
|
||||
next: null; //?
|
||||
comments: Comment[];
|
||||
account: {
|
||||
id: number;
|
||||
username: string;
|
||||
avatar: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Gallery {
|
||||
id: string;
|
||||
title: string;
|
||||
account: Account;
|
||||
media: Media[];
|
||||
tags: Tag[];
|
||||
cover: Media;
|
||||
}
|
||||
|
||||
interface PostTag {
|
||||
name: string;
|
||||
display_name: string;
|
||||
followers: number;
|
||||
total_items: number;
|
||||
following: boolean;
|
||||
is_whitelisted: boolean;
|
||||
background_hash: string;
|
||||
thumbnail_hash: string;
|
||||
accent: string;
|
||||
background_is_animated: boolean;
|
||||
thumbnail_is_animated: boolean;
|
||||
is_promoted: boolean;
|
||||
description: string;
|
||||
logo_hash: string;
|
||||
logo_destination_url: string;
|
||||
description_annotations: {}
|
||||
}
|
||||
|
||||
interface Post {
|
||||
id: string;
|
||||
account_id: number;
|
||||
type: MediaMimeType;
|
||||
animated: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
size: number;
|
||||
views: number;
|
||||
bandwidth: number;
|
||||
vote: null;
|
||||
favorite: boolean;
|
||||
nsfw: boolean;
|
||||
section: string;
|
||||
account_url: string;
|
||||
is_ad: boolean;
|
||||
in_most_viral: boolean;
|
||||
has_sound: boolean;
|
||||
tags: PostTag[];
|
||||
link: string;
|
||||
comment_count: number;
|
||||
ups: number;
|
||||
downs: number;
|
||||
score: number;
|
||||
points: number;
|
||||
is_album: boolean;
|
||||
}
|
||||
|
||||
interface TagResult extends PostTag {
|
||||
items: Post[];
|
||||
success: boolean;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface UserResult {
|
||||
id: number;
|
||||
username: string;
|
||||
bio: string;
|
||||
reputation_count: number;
|
||||
reputation_name: string;
|
||||
avatar_id: string;
|
||||
avatar_url: string;
|
||||
cover_id: string;
|
||||
cover_url: string;
|
||||
created_at: string;
|
||||
}
|
||||
11
src/util.ts
@@ -1,11 +0,0 @@
|
||||
export const proxyURL = (url: string): string =>
|
||||
url.replace(/^https?:\/\/[^.]*\.imgur.com\//, '/');
|
||||
|
||||
export const linkify = (content: string) =>
|
||||
content.replace(
|
||||
/https?:\/\/[^.]*\.imgur.com\/([\/_a-zA-Z0-9-]+)\.(gifv|mp4)/g,
|
||||
'<video src="/$1.mp4" class="commentVideo commentObject" loop="" autoplay="" controls=""></video>'
|
||||
).replace(
|
||||
/https?:\/\/[^.]*\.imgur.com\/([\/_a-zA-Z0-9-]+\.[a-z0-9A-Z]{2,6})/g,
|
||||
'<a href="/$1" target="_blank"><img class="commentImage commentObject" src="/$1" loading="lazy" /></a>'
|
||||
);
|
||||
54
static/css/embed.css
Normal file
@@ -0,0 +1,54 @@
|
||||
.logo {
|
||||
filter: invert(180deg), hue-rotate(180deg);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mediaWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.media {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.media img, .media video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media--gifv {
|
||||
width: 100vw;
|
||||
overflow: unset;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.media--gifv video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.views {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.postDetails {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.postMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 9vh;
|
||||
}
|
||||
BIN
static/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 978 B |
BIN
static/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 967 B |
9
static/favicon/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/static/favicon/mstile-150x150.png"/>
|
||||
<TileColor>#603cba</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
static/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 745 B |
BIN
static/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/favicon/mstile-144x144.png
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
static/favicon/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
static/favicon/mstile-310x150.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/favicon/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/favicon/mstile-70x70.png
Normal file
|
After Width: | Height: | Size: 750 B |
19
static/favicon/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M4904 5445 c-210 -32 -372 -201 -399 -415 -4 -30 -8 -57 -10 -60 -2
|
||||
-3 -665 -5 -1474 -5 l-1471 1 -1 -36 c0 -42 0 -296 0 -326 l1 -22 1510 1 c831
|
||||
0 1513 -2 1516 -5 3 -4 5 -686 5 -1517 0 -831 3 -1511 7 -1512 10 -1 321 -1
|
||||
352 0 l25 1 0 1475 0 1475 25 1 c14 0 34 2 45 4 11 2 35 7 54 10 65 12 163 69
|
||||
221 126 95 96 140 206 140 344 0 230 -184 437 -410 461 -25 3 -49 6 -55 8 -5
|
||||
1 -42 -2 -81 -9z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 917 B |
19
static/favicon/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "rimgo",
|
||||
"short_name": "rimgo",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
1
static/icons/PhArrowFatDown.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M231.39 132.94A8 8 0 0 0 224 128h-40V48a16 16 0 0 0-16-16H88a16 16 0 0 0-16 16v80H32a8 8 0 0 0-5.66 13.66l96 96a8 8 0 0 0 11.32 0l96-96a8 8 0 0 0 1.73-8.72ZM128 220.69L51.31 144H80a8 8 0 0 0 8-8V48h80v88a8 8 0 0 0 8 8h28.69Z"/></svg>
|
||||
|
After Width: | Height: | Size: 347 B |
1
static/icons/PhArrowFatUp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="m229.66 114.34l-96-96a8 8 0 0 0-11.32 0l-96 96A8 8 0 0 0 32 128h40v80a16 16 0 0 0 16 16h80a16 16 0 0 0 16-16v-80h40a8 8 0 0 0 5.66-13.66ZM176 112a8 8 0 0 0-8 8v88H88v-88a8 8 0 0 0-8-8H51.31L128 35.31L204.69 112Z"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
1
static/icons/PhArrowUpRight.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><path fill="currentColor" d="M200 64v104a8 8 0 0 1-16 0V83.31L69.66 197.66a8 8 0 0 1-11.32-11.32L172.69 72H88a8 8 0 0 1 0-16h104a8 8 0 0 1 8 8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 239 B |
1
static/icons/PhChat.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78a.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z"/></svg>
|
||||
|
After Width: | Height: | Size: 376 B |
1
static/icons/PhCheckCircle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0ZM232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104Zm-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88Z"/></svg>
|
||||
|
After Width: | Height: | Size: 355 B |
1
static/icons/PhDevices.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M224 72h-16v-8a24 24 0 0 0-24-24H40a24 24 0 0 0-24 24v96a24 24 0 0 0 24 24h112v8a24 24 0 0 0 24 24h48a24 24 0 0 0 24-24V96a24 24 0 0 0-24-24ZM40 168a8 8 0 0 1-8-8V64a8 8 0 0 1 8-8h144a8 8 0 0 1 8 8v8h-16a24 24 0 0 0-24 24v72Zm192 24a8 8 0 0 1-8 8h-48a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8Zm-96 16a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h40a8 8 0 0 1 8 8Zm80-96a8 8 0 0 1-8 8h-16a8 8 0 0 1 0-16h16a8 8 0 0 1 8 8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 536 B |
1
static/icons/PhEye.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M247.31 124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57 61.26 162.88 48 128 48S61.43 61.26 36.34 86.35C17.51 105.18 9 124 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208s66.57-13.26 91.66-38.34c18.83-18.83 27.3-37.61 27.65-38.4a8 8 0 0 0 0-6.5ZM128 192c-30.78 0-57.67-11.19-79.93-33.25A133.47 133.47 0 0 1 25 128a133.33 133.33 0 0 1 23.07-30.75C70.33 75.19 97.22 64 128 64s57.67 11.19 79.93 33.25A133.46 133.46 0 0 1 231.05 128c-7.21 13.46-38.62 64-103.05 64Zm0-112a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48Zm0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32Z"/></svg>
|
||||
|
After Width: | Height: | Size: 711 B |
1
static/icons/PhGlobe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm-26.37 144h52.74C149 186.34 140 202.87 128 215.89c-12-13.02-21-29.55-26.37-47.89ZM98 152a145.72 145.72 0 0 1 0-48h60a145.72 145.72 0 0 1 0 48Zm-58-24a87.61 87.61 0 0 1 3.33-24h38.46a161.79 161.79 0 0 0 0 48H43.33A87.61 87.61 0 0 1 40 128Zm114.37-40h-52.74C107 69.66 116 53.13 128 40.11c12 13.02 21 29.55 26.37 47.89Zm19.84 16h38.46a88.15 88.15 0 0 1 0 48h-38.46a161.79 161.79 0 0 0 0-48Zm32.16-16h-35.43a142.39 142.39 0 0 0-20.26-45a88.37 88.37 0 0 1 55.69 45ZM105.32 43a142.39 142.39 0 0 0-20.26 45H49.63a88.37 88.37 0 0 1 55.69-45ZM49.63 168h35.43a142.39 142.39 0 0 0 20.26 45a88.37 88.37 0 0 1-55.69-45Zm101.05 45a142.39 142.39 0 0 0 20.26-45h35.43a88.37 88.37 0 0 1-55.69 45Z"/></svg>
|
||||
|
After Width: | Height: | Size: 860 B |
1
static/icons/PhLink.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M137.54 186.36a8 8 0 0 1 0 11.31l-9.94 10a56 56 0 0 1-79.22-79.27l24.12-24.12a56 56 0 0 1 76.81-2.28a8 8 0 1 1-10.64 12a40 40 0 0 0-54.85 1.63L59.7 139.72a40 40 0 0 0 56.58 56.58l9.94-9.94a8 8 0 0 1 11.32 0Zm70.08-138a56.08 56.08 0 0 0-79.22 0l-9.94 9.95a8 8 0 0 0 11.32 11.31l9.94-9.94a40 40 0 0 1 56.58 56.58l-24.12 24.14a40 40 0 0 1-54.85 1.6a8 8 0 1 0-10.64 12a56 56 0 0 0 76.81-2.26l24.12-24.12a56.08 56.08 0 0 0 0-79.24Z"/></svg>
|
||||
|
After Width: | Height: | Size: 549 B |
1
static/icons/PhMagnifyingGlass.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="m229.66 218.34l-50.07-50.06a88.11 88.11 0 1 0-11.31 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32ZM40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72Z"/></svg>
|
||||
|
After Width: | Height: | Size: 265 B |
1
static/icons/PhWarning.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72Zm-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72ZM120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0Zm20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12Z"/></svg>
|
||||
|
After Width: | Height: | Size: 555 B |
1
static/icons/PhWarningCircle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88a88.1 88.1 0 0 1-88 88Zm-8-80V80a8 8 0 0 1 16 0v56a8 8 0 0 1-16 0Zm20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12Z"/></svg>
|
||||
|
After Width: | Height: | Size: 313 B |
1
static/icons/PhWrench.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M226.76 69a8 8 0 0 0-12.84-2.88l-40.3 37.19l-17.23-3.7l-3.7-17.23l37.19-40.3A8 8 0 0 0 187 29.24A72 72 0 0 0 88 96a72.34 72.34 0 0 0 6 28.94L33.79 177c-.15.12-.29.26-.43.39a32 32 0 0 0 45.26 45.26c.13-.13.27-.28.39-.42L131.06 162A72 72 0 0 0 232 96a71.56 71.56 0 0 0-5.24-27ZM160 152a56.14 56.14 0 0 1-27.07-7a8 8 0 0 0-9.92 1.77l-55.9 64.74a16 16 0 0 1-22.62-22.62L109.18 133a8 8 0 0 0 1.77-9.93a56 56 0 0 1 58.36-82.31l-31.2 33.81a8 8 0 0 0-1.94 7.1l5.66 26.33a8 8 0 0 0 6.14 6.14l26.35 5.66a8 8 0 0 0 7.1-1.94l33.81-31.2A56.06 56.06 0 0 1 160 152Z"/></svg>
|
||||
|
After Width: | Height: | Size: 673 B |
56
static/img/rimgo.svg
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
sodipodi:docname="rimgo.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#999999"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.1642403"
|
||||
inkscape:cx="242.64751"
|
||||
inkscape:cy="280.44039"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g1980"
|
||||
transform="translate(113.17535,-113.17535)">
|
||||
<path
|
||||
id="path1096-3"
|
||||
style="fill:none;stroke:#000000;stroke-width:28;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 235.97545,512 V 262 M 0,275.99192 h 250" />
|
||||
<circle
|
||||
style="fill:#1e88e5;fill-opacity:1;stroke:none;stroke-width:22.4636;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path1816"
|
||||
cx="250.86404"
|
||||
cy="261.13596"
|
||||
r="34.785259" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
74
static/tailwind.css
Normal file
@@ -0,0 +1,74 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0 24vw;
|
||||
}
|
||||
|
||||
p a {
|
||||
@apply break-words underline
|
||||
}
|
||||
|
||||
.posts {
|
||||
margin-top: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.posts img:not(.icon),
|
||||
.posts video:not(:fullscreen) {
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
#comments__expandBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comments__expandBtn__label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.comments__expandBtn__icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#comments__expandBtn ~ .comments__expandBtn__label > span::after {
|
||||
content: "⌄";
|
||||
}
|
||||
|
||||
#comments__expandBtn:checked ~ .comments__expandBtn__label > span::after {
|
||||
content: "⌃";
|
||||
}
|
||||
|
||||
#comments__expandBtn:checked ~ .comments {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment__media {
|
||||
height: 12em;
|
||||
display: block;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
body {
|
||||
margin: 0 8vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 812px) {
|
||||
body {
|
||||
margin: 0 4vw;
|
||||
}
|
||||
}
|
||||
8
tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
content: ["./views/*.hbs", "./views/**/*.hbs"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
package types
|
||||
|
||||
type Album struct {
|
||||
Id string
|
||||
Title string
|
||||
Views int64
|
||||
Upvotes int64
|
||||
Downvotes int64
|
||||
SharedWithCommunity bool
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
Comments int64
|
||||
Media []Media
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package types
|
||||
|
||||
type Comment struct {
|
||||
Comments []Comment
|
||||
User User
|
||||
Id string
|
||||
Comment string
|
||||
Upvotes int64
|
||||
Downvotes int64
|
||||
Platform string
|
||||
CreatedAt string
|
||||
RelTime string
|
||||
UpdatedAt string
|
||||
DeletedAt string
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package types
|
||||
|
||||
type Media struct {
|
||||
Id string
|
||||
Name string
|
||||
Title string
|
||||
Description string
|
||||
Url string
|
||||
Type string
|
||||
MimeType string
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package types
|
||||
|
||||
type User struct {
|
||||
Id int64 `json:"id"`
|
||||
Bio string `json:"bio"`
|
||||
Username string `json:"username"`
|
||||
Points int64 `json:"reputation_count"`
|
||||
Cover string `json:"cover_url"`
|
||||
Avatar string `json:"avatar_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type Submission struct {
|
||||
Id string
|
||||
Title string
|
||||
Link string
|
||||
Cover Media
|
||||
Points int64
|
||||
Upvotes int64
|
||||
Downvotes int64
|
||||
Comments int64
|
||||
Views int64
|
||||
IsAlbum bool
|
||||
}
|
||||
65
utils/config.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Port string
|
||||
Addr string
|
||||
ImgurId string
|
||||
FiberPrefork bool
|
||||
ImageCache bool
|
||||
CleanupInterval time.Duration
|
||||
CacheDir string
|
||||
Privacy map[string]interface{}
|
||||
}
|
||||
|
||||
var Config config
|
||||
|
||||
func LoadConfig() {
|
||||
port := "3000"
|
||||
if os.Getenv("PORT") != "" {
|
||||
port = os.Getenv("PORT")
|
||||
}
|
||||
if os.Getenv("RIMGU_PORT") != "" {
|
||||
port = os.Getenv("RIMGU_PORT")
|
||||
}
|
||||
|
||||
addr := "0.0.0.0"
|
||||
if os.Getenv("ADDRESS") != "" {
|
||||
addr = os.Getenv("ADDRESS")
|
||||
}
|
||||
if os.Getenv("RIMGU_ADDRESS") != "" {
|
||||
addr = os.Getenv("RIMGU_ADDRESS")
|
||||
}
|
||||
|
||||
imgurId := "546c25a59c58ad7"
|
||||
if os.Getenv("IMGUR_CLIENT_ID") != "" {
|
||||
imgurId = os.Getenv("IMGUR_CLIENT_ID")
|
||||
}
|
||||
if os.Getenv("RIMGU_IMGUR_CLIENT_ID") != "" {
|
||||
imgurId = os.Getenv("RIMGU_IMGUR_CLIENT_ID")
|
||||
}
|
||||
|
||||
Config = config{
|
||||
Port: port,
|
||||
Addr: addr,
|
||||
ImgurId: imgurId,
|
||||
FiberPrefork: os.Getenv("FIBER_PREFORK") == "true",
|
||||
Privacy: map[string]interface{}{
|
||||
"set": os.Getenv("PRIVACY_NOT_COLLECTED") != "",
|
||||
"policy": os.Getenv("PRIVACY_POLICY"),
|
||||
"message": os.Getenv("PRIVACY_MESSAGE"),
|
||||
"country": os.Getenv("PRIVACY_COUNTRY"),
|
||||
"provider": os.Getenv("PRIVACY_PROVIDER"),
|
||||
"cloudflare": os.Getenv("PRIVACY_CLOUDFLARE") == "true",
|
||||
"not_collected": os.Getenv("PRIVACY_NOT_COLLECTED") == "true",
|
||||
"ip": os.Getenv("PRIVACY_IP") == "true",
|
||||
"url": os.Getenv("PRIVACY_URL") == "true",
|
||||
"device": os.Getenv("PRIVACY_DEVICE") == "true",
|
||||
"diagnostics": os.Getenv("PRIVACY_DIAGNOSTICS") == "true",
|
||||
},
|
||||
}
|
||||
}
|
||||
5
utils/regex.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package utils
|
||||
|
||||
import "regexp"
|
||||
|
||||
var ImgurRe = regexp.MustCompile(`https?://(i\.)?imgur\.com`)
|
||||
46
utils/request.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func GetJSON(url string) (gjson.Result, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return gjson.Result{}, err
|
||||
}
|
||||
|
||||
SetReqHeaders(req)
|
||||
|
||||
client := http.Client{}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return gjson.Result{}, err
|
||||
}
|
||||
rateLimitRemaining := res.Header.Get("X-RateLimit-UserRemaining")
|
||||
if rateLimitRemaining != "" {
|
||||
ratelimit, _ := strconv.Atoi(rateLimitRemaining)
|
||||
if ratelimit <= 0 {
|
||||
return gjson.Result{}, fmt.Errorf("ratelimited by imgur")
|
||||
}
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return gjson.Result{}, err
|
||||
}
|
||||
|
||||
switch (res.StatusCode) {
|
||||
case 200:
|
||||
return gjson.Parse(string(body)), nil
|
||||
case 429:
|
||||
return gjson.Result{}, fmt.Errorf("ratelimited by imgur")
|
||||
default:
|
||||
return gjson.Result{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,28 @@
|
||||
package utils
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func SetHeaders(c *fiber.Ctx) {
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Referrer-Policy", "no-referrer")
|
||||
c.Set("X-Content-Type-Options", "nosniff")
|
||||
c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
|
||||
c.Set("Strict-Transport-Security", "max-age=31557600")
|
||||
c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
|
||||
c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
|
||||
}
|
||||
|
||||
func SetReqHeaders(req *http.Request) {
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Origin", "https://imgur.com")
|
||||
req.Header.Set("Pragma", "no-cache")
|
||||
req.Header.Set("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Set("Sec-Fetch-Site", "same-site")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0")
|
||||
}
|
||||
110
views/about.hbs
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>About - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<section class="my-4 w-full flex flex-col items-center">
|
||||
{{> partials/searchBar }}
|
||||
</section>
|
||||
|
||||
|
||||
<main class="my-8">
|
||||
<p>An alternative frontend for Imgur. Originally based on <a href="https://codeberg.org/3np/rimgu" rel="noreferrer">rimgu</a>.</p>
|
||||
|
||||
<h2 class="font-bold text-2xl my-2">Features</h2>
|
||||
<ul class="list-disc ml-4">
|
||||
<li>Lightweight</li>
|
||||
<li>No JavaScript</li>
|
||||
<li>No ads or tracking</li>
|
||||
<li>No sign up or app install prompts</li>
|
||||
<li>Bandwidth efficient - automatically uses newer image formats (if enabled)</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="font-bold text-2xl my-2">Comparison</h2>
|
||||
<p>Comparing rimgo to Imgur.</p>
|
||||
|
||||
<h3 class="font-bold text-xl my-2">Speed</h3>
|
||||
<p>Tested using <a href="https://pagespeed.web.dev/" rel="nofollow noreferrer">Google PageSpeed Insights</a>.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a href="https://pagespeed.web.dev/report?url=https%3A%2F%2Fi.bcow.xyz%2Fgallery%2FgYiQLWy" rel="nofollow noreferrer">rimgo</a></td>
|
||||
<td><a href="https://pagespeed.web.dev/report?url=https%3A%2F%2Fimgur.com%2Fgallery%2FgYiQLWy" rel="nofollow noreferrer">Imgur</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Performance</td>
|
||||
<td>91</td>
|
||||
<td>28</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Request count</td>
|
||||
<td>29</td>
|
||||
<td>340</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Resource Size</td>
|
||||
<td>218 KiB</td>
|
||||
<td>2,542 KiB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Time to Interactive</td>
|
||||
<td>1.6s</td>
|
||||
<td>23.8s</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 class="font-bold text-xl my-2">Privacy</h3>
|
||||
<p>Imgur collects information about your device and uses tracking cookies for advertising, this is mentioned in their <a href="https://imgur.com/privacy/" rel="nofollow noreferrer">privacy policy</a>. <a href="https://themarkup.org/blacklight" rel="nofollow noreferrer">Blacklight</a> found 31 trackers and 87 third-party cookies.</p>
|
||||
<p>See what cookies and trackers Imgur uses and where your data gets sent: <a href="https://themarkup.org/blacklight?url=imgur.com" rel="nofollow noreferrer">https://themarkup.org/blacklight?url=imgur.com</a></p>
|
||||
|
||||
<h2 class="font-bold text-2xl my-2">Usage</h2>
|
||||
<p>Just replace imgur.com or i.imgur.com with <code>{{domain}}</code> (add stack/ before the media ID for i.stack.imgur.com). You can setup automatic redirects using <a href="https://github.com/libredirect/libredirect" rel="nofollow noreferrer">LibRedirect</a> (recommended) or <a href="https://github.com/einaregilsson/Redirector" rel="nofollow noreferrer">Redirector</a>.</p>
|
||||
|
||||
{{#if force_webp}}
|
||||
<p>To download images as their original filetype, add <code>?no_webp=1</code> to the end of the image URL.</p>
|
||||
{{/if}}
|
||||
|
||||
<h2 class="font-bold text-2xl my-2">Automatically redirect links</h2>
|
||||
|
||||
<h3 class="font-bold text-xl my-2">LibRedirect</h3>
|
||||
<p>Use <a href="https://github.com/libredirect/libredirect" rel="nofollow noreferrer">LibRedirect</a> to automatically redirect Imgur links to rimgo!</p>
|
||||
|
||||
<h3 class="font-bold text-xl my-2">GreaseMonkey script</h3>
|
||||
<p>There is a script to redirect Imgur links to rimgo. <a rel="noreferrer" href="https://codeberg.org/zortazert/GreaseMonkey-Redirect/src/branch/main/imgur-to-rimgo.user.js">https://codeberg.org/zortazert/GreaseMonkey-Redirect/src/branch/main/imgur-to-rimgo.user.js</a></p>
|
||||
|
||||
<h3 class="font-bold text-xl my-2">Redirector</h3>
|
||||
<p>You can use the <a href="https://github.com/einaregilsson/Redirector" rel="nofollow noreferrer">Redirector</a> extension to redirect Imgur links to rimgo with the configuration below:</p>
|
||||
<ul class="list-disc ml-4">
|
||||
<li>Description: Imgur -> rimgo</li>
|
||||
<li>Example URL: https://imgur.com/a/H8M4rcp</li>
|
||||
<li>Include pattern: <code>^https?://i?.?imgur.com(/.*)?$</code></li>
|
||||
<li>Redirect to: <code>{{proto}}://{{domain}}$1</code></li>
|
||||
<li>Pattern type: Regular Expression</li>
|
||||
<li>Advanced Options > Apply to: Check Images</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<p>For Stack Overflow Images:</p>
|
||||
<ul class="list-disc ml-4">
|
||||
<li>Description: Stack Overflow Imgur -> rimgo</li>
|
||||
<li>Example URL: https://i.stack.imgur.com/BTKqD.png?s=128&g=1</li>
|
||||
<li>Include pattern: <code>^https?://i\.stack\.imgur\.com(/.*)?$</code></li>
|
||||
<li>Redirect to: <code>{{proto}}://{{domain}}/stack$1</code></li>
|
||||
<li>Pattern type: Regular Expression</li>
|
||||
<li>Advanced Options > Apply to: Check Images</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="font-bold text-2xl my-2">Notice</h2>
|
||||
<p>All images and media are from Imgur. rimgo does not allow uploads or comments. Any issues with content should be reported to Imgur.</p>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
48
views/embed.hbs
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
{{> partials/head }}
|
||||
|
||||
<link rel="stylesheet" href="/static/css/embed.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="mediaWrapper">
|
||||
<div class="media">
|
||||
{{#each post.Media}}
|
||||
{{#equal this.Type "image"}}
|
||||
<img src="{{this.Url}}" loading="lazy">
|
||||
{{/equal}}
|
||||
{{#equal this.Type "video"}}
|
||||
<video controls loop>
|
||||
<source type="{{this.MimeType}}" src="{{this.Url}}" />
|
||||
</video>
|
||||
{{/equal}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="postMeta">
|
||||
<div class="postDetails">
|
||||
{{#if post.TItle}}
|
||||
<a href="/{{post.Id}}">
|
||||
<h3>{{post.Title}}</h3>
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="views flex flex-center">
|
||||
<img class="icon" src="/static/icons/eye.svg" alt="Views">
|
||||
<p>{{post.Views}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-center">
|
||||
<a href="/{{post.Id}}">
|
||||
<img src="/static/img/rimgo.svg" width="32px" height="32px" class="logo">
|
||||
</a>
|
||||
<a href="/{{post.Id}}">
|
||||
rimgo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
21
views/errors/404.hbs
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Not Found - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<main>
|
||||
<h2 class="text-2xl font-bold">Not Found</h2>
|
||||
<p>Click <a href="/">here</a> to return to home.</p>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
21
views/errors/429.hbs
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Rate limited by Imgur - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<main>
|
||||
<h2 class="text-2xl font-bold">Rate limited by Imgur</h2>
|
||||
<p>This instance has been temporarily blocked by Imgur. <a href="https://rimgo.codeberg.page/#{{ path }}">Try using another instance ></a></p>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
22
views/errors/error.hbs
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Error - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<main class="flex flex-col">
|
||||
<h2 class="text-2xl font-bold">An error occurred</h2>
|
||||
<p>You may have found a bug in rimgo. If this is a bug, open an issue on <a href="https://codeberg.org/video-prize-ranch/rimgo/issues/new">Codeberg</a>.</p>
|
||||
<code class="mt-4 p-2 bg-slate-600 rounded-md">{{err}}</code>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -5,54 +5,49 @@
|
||||
<title>rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
|
||||
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/css/frontpage.css" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> partials/header }}
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<main>
|
||||
<p>An alternative frontend for Imgur. Based on <a href="https://codeberg.org/3np/rimgu">rimgu</a> and rewritten in Go.</p>
|
||||
|
||||
<p>It's read-only and works without JavaScript. Images and albums can be viewed without wasting resources from downloading and running tracking scripts. No sign-up nags.</p>
|
||||
|
||||
<h3>Try it!</h3>
|
||||
|
||||
<ul>
|
||||
<li><a href="/a/H8M4rcp">Album</a></li>
|
||||
<li><a href="/gallery/gYiQLWy">Gallery</a></li>
|
||||
<li><a href="/gallery/cTRwaJU">Video</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Features</h2>
|
||||
|
||||
<ul>
|
||||
<li>URL-compatible with i.imgur.com - just replace the domain in the URL</li>
|
||||
<li>Images and videos (gifv, mp4)</li>
|
||||
<li>Albums</li>
|
||||
<li>Streaming</li>
|
||||
<li>Pretty CSS styling (responsive!)</li>
|
||||
<li>Logo</li>
|
||||
</ul>
|
||||
|
||||
<h2>Usage</h2>
|
||||
|
||||
<p>Just replace imgur.com with the domain of this instance! You can setup automatic redirects using <a href="https://github.com/einaregilsson/Redirector">Redirector</a>.</p>
|
||||
|
||||
<h3>Redirector configuration</h3>
|
||||
|
||||
<ul>
|
||||
<li>Description: Imgur -> rimgo</li>
|
||||
<li>Example URL: https://imgur.com/a/H8M4rcp</li>
|
||||
<li>Include pattern: <code>^https?://(i|).?imgur.com(/.*)?$</code></li>
|
||||
<li>Redirect to: <code>https://[INSTANCE DOMAIN]$2</code></li>
|
||||
<li>Pattern type: Regular Expression</li>
|
||||
</ul>
|
||||
<header class="my-8 p-8 rounded-xl flex flex-col gap-4 items-center justify-center bg-gradient-to-r from-blue-400 to-emerald-400">
|
||||
<h2 class="font-bold text-white text-2xl">The fast, private image viewer for Imgur.</h2>
|
||||
{{> partials/searchBar }}
|
||||
<a class="flex gap-1 items-center" href="/trending">Or see what's trending <img class="invert" src="/static/icons/PhArrowUpRight.svg" alt="" height="18" width="18" /></a>
|
||||
</header>
|
||||
|
||||
<main class="my-8">
|
||||
<h2 class="font-bold text-2xl">What is rimgo?</h2>
|
||||
<p>
|
||||
rimgo is an alternative frontend for Imgur.
|
||||
Originally based on <a href="https://codeberg.org/3np/rimgu" rel="noreferrer">rimgu</a>.
|
||||
It's a way to access Imgur without the ads, tracking, and other garbage.
|
||||
rimgo is not affiliated with Imgur, all content is proxied from Imgur.
|
||||
</p>
|
||||
<br/>
|
||||
<h3 class="font-bold text-xl">Notice</h3>
|
||||
<p>All images and media are from Imgur. rimgo does not allow uploads or comments. Any issues with content should be reported to Imgur.</p>
|
||||
</main>
|
||||
|
||||
<h2 class="font-bold text-2xl">This instance</h2>
|
||||
<section class="my-4 lg:m-4 flex flex-col lg:flex-row gap-4 items-center">
|
||||
{{> partials/privacy }}
|
||||
<div class="self-start">
|
||||
<h2 class="text-xl my-2 font-bold">Additional information</h2>
|
||||
<ul>
|
||||
<li>Version: {{version}}</li>
|
||||
<li>Country: {{config.Privacy.country}}</li>
|
||||
<li>Provider: {{config.Privacy.provider}}</li>
|
||||
{{#if config.Privacy.cloudflare}}
|
||||
<li>Using Cloudflare?: Yes</li>
|
||||
{{else}}
|
||||
<li>Using Cloudflare?: No</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<p><a href="/privacy">Learn more about instance privacy ></a></p>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{album.Title}} - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
|
||||
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/fonts/Material-Icons-Outlined.css" />
|
||||
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/css/album.css" />
|
||||
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/css/comments.css" />
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> partials/header }}
|
||||
|
||||
<main>
|
||||
<h1>{{album.Title}}</h1>
|
||||
|
||||
<p>{{album.CreatedAt}}</p>
|
||||
|
||||
<div class="imageMeta__wrapper">
|
||||
<div class="imageMeta">
|
||||
<div class="imageMeta__item">
|
||||
<span class="material-icons-outlined" title="Views">visibility</span>
|
||||
<p>{{album.Views}}</p>
|
||||
</div>
|
||||
{{#if album.SharedWithCommunity}}
|
||||
<p><span class="material-icons-outlined" title="Likes">thumb_up</span> {{album.Upvotes}}</p>
|
||||
<p><span class="material-icons-outlined" title="Dislilkes">thumb_down</span> {{album.Downvotes}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<!--<div class="videoDesc__channel">
|
||||
<a href="{{claim.Channel.RelUrl}}">
|
||||
{{#if claim.Channel.Thumbnail}}
|
||||
<img src="{{claim.Channel.Thumbnail}}&w=56&h=56" class="pfp" width="56" height="56" loading="lazy" />
|
||||
{{/if}}
|
||||
</a>
|
||||
<a href="{{claim.Channel.RelUrl}}">
|
||||
<p>
|
||||
<b>{{claim.Channel.Title}}</b>
|
||||
</p>
|
||||
</a>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
{{#each album.Media}}
|
||||
{{#if this.Title}}
|
||||
<h3>{{this.Title}}</h3>
|
||||
{{/if}}
|
||||
{{#if this.Description}}
|
||||
<p>{{this.Description}}</p><br>
|
||||
{{/if}}
|
||||
|
||||
<div class="center">
|
||||
{{#equal this.Type "image"}}
|
||||
<img src="{{this.Url}}" loading="lazy">
|
||||
{{/equal}}
|
||||
{{#equal this.Type "video"}}
|
||||
<video controls loop>
|
||||
<source type="{{this.MimeType}}" src="{{this.Url}}" />
|
||||
</video>
|
||||
{{/equal}}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{{/each}}
|
||||
|
||||
{{#if comments}}
|
||||
<div>
|
||||
<hr>
|
||||
<input id="comments__expandBtn" type="checkbox">
|
||||
<label class="comments__expandBtn__label" for="comments__expandBtn">
|
||||
<h3>Comments ({{album.Comments}})</h3>
|
||||
<span class="comments__expandBtn__icon material-icons-outlined"></span>
|
||||
</label>
|
||||
<div class="comments">
|
||||
{{#each comments}}
|
||||
{{> partials/comment }}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{{/if}}
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
32
views/gifv.hbs
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
{{> partials/head }}
|
||||
|
||||
<link rel="stylesheet" href="/static/css/embed.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="mediaWrapper">
|
||||
<div class="media media--gifv">
|
||||
<video loop poster="/{{id}}.webp" autoplay controls>
|
||||
<source src="/{{id}}.webm" type="video/webm" />
|
||||
<source src="/{{id}}.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div class="postMeta">
|
||||
<a href="/{{id}}.mp4" download="{{id}}.mp4">download</a>
|
||||
<div class="flex flex-center">
|
||||
<a href="/{{post.Id}}">
|
||||
<img src="/static/img/rimgo.svg" width="32px" height="32px" class="logo">
|
||||
</a>
|
||||
<a href="/{{post.Id}}">
|
||||
rimgo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,25 +1,32 @@
|
||||
<div class="comment">
|
||||
<div class="comment__user">
|
||||
<img src="{{this.User.Avatar}}" class="pfp" width="24" height="24" loading="lazy">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
{{#noteq this.User.Username "[deleted]"}}
|
||||
<img src="{{this.User.Avatar}}" class="rounded-full" width="24" height="24" loading="lazy">
|
||||
<a href="/user/{{this.User.Username}}">
|
||||
<p class="comment__user__username"><b>{{this.User.Username}}</b></p>
|
||||
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.User.Username}}</b></p>
|
||||
</a>
|
||||
{{/noteq}}
|
||||
{{#equal this.User.Username "[deleted]"}}
|
||||
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
|
||||
{{/equal}}
|
||||
</div>
|
||||
<div>
|
||||
{{{this.Comment}}}
|
||||
<p>
|
||||
<p>{{{this.Comment}}}</p>
|
||||
<div class="flex gap-2">
|
||||
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
|
||||
{{#if this.DeletedAt}}
|
||||
<span class="comment__updatedDate">(deleted {{this.DeletedAt}})</span>
|
||||
<span class="text-md">(deleted {{this.DeletedAt}})</span>
|
||||
{{/if}}
|
||||
|
|
||||
<span class="material-icons-outlined">thumb_up</span> {{this.Upvotes}}
|
||||
<span class="material-icons-outlined">thumb_down</span> {{this.Downvotes}}
|
||||
</p>
|
||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> {{this.Upvotes}}
|
||||
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px"> {{this.Downvotes}}
|
||||
</div>
|
||||
<div class="replies">
|
||||
</div>
|
||||
{{#if this.Comments}}
|
||||
<div class="ml-4 p-2 border-solid border-l-2 border-slate-400">
|
||||
{{#each this.Comments}}
|
||||
{{> partials/comment }}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
22
views/partials/contextComment.hbs
Normal file
@@ -0,0 +1,22 @@
|
||||
<a href="{{Post.Link}}">
|
||||
<div class="sm:grid gap-4 w-full" style="grid-template-columns: 120px 1fr;">
|
||||
<img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt="">
|
||||
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<p class="md-container">{{{this.Comment}}}</p>
|
||||
<div class="flex-grow"></div>
|
||||
<div class="flex gap-2">
|
||||
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
|
||||
{{#if this.DeletedAt}}
|
||||
<span class="text-md">(deleted {{this.DeletedAt}})</span>
|
||||
{{/if}}
|
||||
|
|
||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
||||
{{this.Upvotes}}
|
||||
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
||||
{{this.Downvotes}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,10 +1,14 @@
|
||||
<br><br><br>
|
||||
|
||||
<footer>
|
||||
<!--<a href="/privacy">
|
||||
<span class="material-icons-outlined">visibility</span> Privacy
|
||||
</a>-->
|
||||
<a href="https://codeberg.org/video-prize-ranch/rimgo" rel="noreferrer">
|
||||
<span class="material-icons-outlined">code</span> Source Code
|
||||
<footer class="mt-24 mb-12">
|
||||
<div class="mb-1 flex gap-4 font-bold">
|
||||
<a href="/">
|
||||
<div class="flex">
|
||||
<img src="/static/img/rimgo.svg" class="invert hue-rotate-180" width="32" height="32" />
|
||||
<h1 class="text-md font-extrabold">rimgo</h1>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://codeberg.org/rimgo/rimgo">Source Code</a>
|
||||
<a href="/about">About</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
</div>
|
||||
<p class="text-md text-slate-200">rimgo does not allow uploads or host any content. Issues with content should be reported to Imgur.</p>
|
||||
</footer>
|
||||
@@ -2,15 +2,14 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/site.webmanifest">
|
||||
<link rel="mask-icon" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/safari-pinned-tab.svg" color="#1e88e5">
|
||||
<link rel="shortcut icon" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/favicon.ico">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
||||
<link rel="mask-icon" href="/static/favicon/safari-pinned-tab.svg" color="#1e88e5">
|
||||
<link rel="shortcut icon" href="/static/favicon/favicon.ico">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="msapplication-config" content="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/browserconfig.xml">
|
||||
<meta name="msapplication-config" content="/static/favicon/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/fonts/Material-Icons-Outlined.css" />
|
||||
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/css/base.css"/>
|
||||
<link rel="stylesheet" href="/static/app.css" />
|
||||
@@ -1,8 +0,0 @@
|
||||
<nav>
|
||||
<a href="/">
|
||||
<img src="https://d12bxz4wjlvqjb.cloudfront.net/static/img/rimgo.svg" width="64" height="64" class="logo">
|
||||
</a>
|
||||
<a href="/">
|
||||
<h2>rimgo</h2>
|
||||
</a>
|
||||
</nav>
|
||||
6
views/partials/nav.hbs
Normal file
@@ -0,0 +1,6 @@
|
||||
<a href="/">
|
||||
<nav class="m-4 flex items-center justify-center">
|
||||
<img src="/static/img/rimgo.svg" class="invert hue-rotate-180" width="72" height="72" />
|
||||
<h1 class="font-extrabold text-3xl">rimgo</h1>
|
||||
</nav>
|
||||
</a>
|
||||
@@ -1,32 +1,27 @@
|
||||
<a href="{{Link}}">
|
||||
<div class="post">
|
||||
<div class="bg-slate-600 rounded-lg">
|
||||
{{#equal Cover.Type "video"}}
|
||||
<video fullscreen controls loop poster="/{{Cover.Id}}.webp" preload="none" width="100%" height="100%">
|
||||
<video controls loop poster="/{{Cover.Id}}.webp" preload="none" width="100%" height="100%">
|
||||
<source src="{{Cover.Url}}" type="video/mp4" />
|
||||
</video>
|
||||
{{/equal}}
|
||||
{{#equal Cover.Type "image"}}
|
||||
<img src="{{Cover.Url}}" loading="lazy" width="100%" height="100%">
|
||||
{{/equal}}
|
||||
<p class="post__title">{{Title}}</p>
|
||||
<div class="post__meta">
|
||||
<p>
|
||||
<span class="material-icons-outlined">
|
||||
arrow_upward
|
||||
</span>
|
||||
<p class="m-2 text-ellipsis whitespace-nowrap overflow-hidden">{{Title}}</p>
|
||||
<div class="flex gap-2 p-2">
|
||||
<div class="flex gap-1">
|
||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Points" width="18px" height="18px">
|
||||
{{Points}}
|
||||
</p>
|
||||
<p>
|
||||
<span class="material-icons-outlined">
|
||||
comment
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img class="invert icon" src="/static/icons/PhChat.svg" alt="Comments" width="18px" height="18px">
|
||||
{{Comments}}
|
||||
<p>
|
||||
<span class="material-icons-outlined">
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img class="invert icon" src="/static/icons/PhEye.svg" alt="Views" width="18px" height="18px">
|
||||
{{Views}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
48
views/partials/privacy.hbs
Normal file
@@ -0,0 +1,48 @@
|
||||
{{#if config.Privacy.not_collected}}
|
||||
<div class="flex flex-col items-center w-full lg:w-1/2 p-4 bg-slate-600 rounded-lg">
|
||||
<img class="invert" src="/static/icons/PhCheckCircle.svg" alt="" height="36" width="36" />
|
||||
<h2 class="font-bold text-xl">Data not collected</h2>
|
||||
<p class="text-lg">This instance does not collect any data.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#unless config.Privacy.set}}
|
||||
<div class="flex flex-col items-center w-full lg:w-1/2 p-4 bg-slate-600 rounded-lg">
|
||||
<img class="invert" src="/static/icons/PhWarning.svg" alt="" height="36" width="36" />
|
||||
<h2 class="font-bold text-xl">No details provided</h2>
|
||||
<p class="text-lg">The operator of this instance will be required to provide privacy details to be added to the instance list.</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#unless config.Privacy.not_collected}}
|
||||
<div class="flex flex-col items-center w-full lg:w-1/2 p-4 bg-slate-600 rounded-lg">
|
||||
<img class="invert" src="/static/icons/PhWarningCircle.svg" alt="" height="36" width="36" />
|
||||
<h2 class="font-bold text-xl">Data collected</h2>
|
||||
<p class="text-lg">The following data may be collected:</p>
|
||||
<ul class="flex flex-col">
|
||||
{{#if config.Privacy.ip}}
|
||||
<li class="flex gap-1">
|
||||
<img class="invert" src="/static/icons/PhGlobe.svg" alt="" width="24px" height="24px" />
|
||||
Internet address (IP Address)
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if config.Privacy.url}}
|
||||
<li class="flex gap-1">
|
||||
<img class="invert" src="/static/icons/PhLink.svg" alt="" width="24px" height="24px" />
|
||||
Page viewed (Request URL)
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if config.Privacy.device}}
|
||||
<li class="flex gap-1">
|
||||
<img class="invert" src="/static/icons/PhDevices.svg" alt="" width="24px" height="24px" />
|
||||
Device Type (User agent)
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if config.Privacy.diagnostics}}
|
||||
<li class="flex gap-1">
|
||||
<img class="invert" src="/static/icons/PhWrench.svg" alt="" width="24px" height="24px" />
|
||||
Diagnostics
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
26
views/partials/result.hbs
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="bg-slate-600 rounded-lg">
|
||||
<a href="{{Url}}">
|
||||
<img src="{{ImageUrl}}?no_webp=1" loading="lazy" width="100%" height="100%">
|
||||
</a>
|
||||
<p class="m-2 text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<a href="{{Url}}" title="{{Title}}">{{Title}}</a><br />
|
||||
{{#if User}}
|
||||
by <a href="/user/{{User}}" title="{{User}}">{{User}}</a>
|
||||
{{else}}
|
||||
<br />
|
||||
{{/if}}
|
||||
</p>
|
||||
<div class="flex gap-2 p-2">
|
||||
<p title="{{RelTime}}" class="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
{{RelTime}}
|
||||
</p>
|
||||
<div class="flex gap-1">
|
||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Points" width="18px" height="18px">
|
||||
{{Points}}
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<img class="invert icon" src="/static/icons/PhEye.svg" alt="Views" width="18px" height="18px">
|
||||
{{Views}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
6
views/partials/searchBar.hbs
Normal file
@@ -0,0 +1,6 @@
|
||||
<form method="GET" action="/search" class="flex gap-2 w-full lg:w-1/2">
|
||||
<input class="p-2 rounded-lg bg-slate-600 w-full" name="q" type="text" placeholder="Search or paste a link" {{#if query}}value="{{query}}"{{/if}}/>
|
||||
<button type="submit" class="p-2 rounded-lg bg-slate-600">
|
||||
<img class="invert icon" src="/static/icons/PhMagnifyingGlass.svg" alt="Search" width="24px" height="24px">
|
||||
</button>
|
||||
</form>
|
||||
120
views/post.hbs
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
{{#if post.Title}}
|
||||
{{post.Title}} -
|
||||
{{/if}}
|
||||
rimgo
|
||||
</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<section class="my-4 w-full flex flex-col items-center">
|
||||
{{> partials/searchBar }}
|
||||
</section>
|
||||
|
||||
<header>
|
||||
<h1 class="text-3xl font-bold">{{post.Title}}</h1>
|
||||
<p>{{post.CreatedAt}}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center my-4">
|
||||
{{#if post.User.Username}}
|
||||
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
|
||||
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
|
||||
<p>
|
||||
<b>{{post.User.Username}}</b>
|
||||
</p>
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
|
||||
<p>{{post.Views}}</p>
|
||||
</div>
|
||||
{{#if post.SharedWithCommunity}}
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
||||
<p>{{post.Upvotes}}</p>
|
||||
</div>
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
||||
<p>{{post.Downvotes}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-center flex-col">
|
||||
{{#each post.Media}}
|
||||
{{#if this.Title}}
|
||||
<h4 class="font-bold">{{this.Title}}</h4>
|
||||
{{/if}}
|
||||
|
||||
{{#equal this.Type "image"}}
|
||||
<img class="my-2 max-h-96 object-contain" src="{{this.Url}}" loading="lazy">
|
||||
{{/equal}}
|
||||
{{#equal this.Type "video"}}
|
||||
<video class="my-2 max-h-96 object-contain" controls loop>
|
||||
<source type="{{this.MimeType}}" src="{{this.Url}}" />
|
||||
</video>
|
||||
{{/equal}}
|
||||
|
||||
{{#if this.Description}}
|
||||
<p>{{{this.Description}}}</p>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#if post.tags}}
|
||||
<div class="flex gap-2 my-2 flex-wrap">
|
||||
<style nonce="{{nonce}}">
|
||||
{{#each post.tags}}
|
||||
.{{this.BackgroundId}} { background-image: url('{{this.Background}}') }
|
||||
{{/each}}
|
||||
</style>
|
||||
{{#each post.tags}}
|
||||
<a href="/t/{{this.Tag}}">
|
||||
<div class="rounded-md p-4 min-w-[110px] bg-slate-500 {{this.BackgroundId}}">
|
||||
<p class="font-bold text-white text-center">
|
||||
{{#if tag.Display}}
|
||||
{{this.Display}}
|
||||
{{else}}
|
||||
{{this.Tag}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</main>
|
||||
|
||||
<section>
|
||||
{{#if comments}}
|
||||
<div>
|
||||
<input id="comments__expandBtn" type="checkbox" checked>
|
||||
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" for="comments__expandBtn">
|
||||
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
|
||||
<span class="text-xl font-bold"></span>
|
||||
</label>
|
||||
<div class="comments flex flex-col gap-2">
|
||||
{{#each comments}}
|
||||
{{> partials/comment }}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
78
views/privacy.hbs
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Privacy - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<section class="my-4 w-full flex flex-col items-center">
|
||||
{{> partials/searchBar }}
|
||||
</section>
|
||||
|
||||
<header>
|
||||
<h1 class="text-3xl font-bold">Instance Privacy</h1>
|
||||
{{#if config.Privacy.policy}}
|
||||
<p>The instance operator has indicated their instance's privacy practices below. For more information, see the
|
||||
instance operator's <a href="{{config.Privacy.policy}}">privacy policy</a>.</p>
|
||||
{{else}}
|
||||
<p>The instance operator has indicated their instance's privacy practices below.</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if config.Privacy.message}}
|
||||
<p class="my-4">{{{config.Privacy.message}}}</p>
|
||||
{{/if}}
|
||||
</header>
|
||||
|
||||
<main class="flex justify-center w-full">
|
||||
{{> partials/privacy }}
|
||||
</main>
|
||||
|
||||
<section>
|
||||
<h2 class="text-2xl my-4 font-bold">What is instance privacy?</h2>
|
||||
<p>Instance privacy aims to bring transparency to the data collected by frontends and encourage privacy friendly practices. There is often no privacy policy and users are forced to trust that the instance operator is not collecting data. However, there is a possibility that the instance operator can put false information so we encourage looking at other factors when selecting an instance.</p>
|
||||
<ul class="flex flex-col list-outside list-disc mt-2 ml-4">
|
||||
<li>
|
||||
<img class="inline invert" src="/static/icons/PhGlobe.svg" alt="" width="24px" height="24px" />
|
||||
Internet address (IP Address) - This is an address that is given to your computer or internet connection.
|
||||
This can be used to find your city or region and internet provider but can change depending on your connection.
|
||||
</li>
|
||||
<li>
|
||||
<img class="inline invert" src="/static/icons/PhLink.svg" alt="" width="24px" height="24px" />
|
||||
Page viewed (Request URL) - This is what page you are viewing.
|
||||
</li>
|
||||
<li>
|
||||
<img class="inline invert" src="/static/icons/PhDevices.svg" alt="" width="24px" height="24px" />
|
||||
Device Type (User agent) - This is what browser, device, and operating system you are using.
|
||||
Advanced users can change this with an extension or browser setting.
|
||||
</li>
|
||||
<li>
|
||||
<img class="inline invert" src="/static/icons/PhWrench.svg" alt="" width="24px" height="24px" />
|
||||
Diagnostics - When this data is only collected when there is an error or only a short amount of time while
|
||||
diagnosing an issue.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-2xl my-4 font-bold">Additional information</h2>
|
||||
<ul>
|
||||
<li>Version: {{version}}</li>
|
||||
<li>Country: {{config.Privacy.country}}</li>
|
||||
<li>Provider: {{config.Privacy.provider}}</li>
|
||||
{{#if config.Privacy.cloudflare}}
|
||||
<li>Using Cloudflare?: Yes</li>
|
||||
{{else}}
|
||||
<li>Using Cloudflare?: No</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{> 'partials/footer' }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
36
views/search.hbs
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{query}} - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<section class="my-4 w-full flex flex-col items-center">
|
||||
{{> partials/searchBar }}
|
||||
</section>
|
||||
|
||||
<main>
|
||||
<div class="posts">
|
||||
{{#each results}}
|
||||
{{> partials/result }}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-4 font-bold">
|
||||
{{#if displayPrev}}
|
||||
<a href="/search?q={{query}}&page={{prevPage}}">Previous page</a>
|
||||
{{/if}}
|
||||
<p>Page {{page}}</p>
|
||||
<a href="/search?q={{query}}&page={{nextPage}}">Next page</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
59
views/tag.hbs
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{tag.Display}} - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<section class="my-4 w-full flex flex-col items-center">
|
||||
{{> partials/searchBar }}
|
||||
</section>
|
||||
|
||||
<header class="p-4 rounded-xl text-white mb-4" style="background-image: url('{{tag.Background}}');">
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<h2 class="text-2xl font-bold">{{tag.Display}}</h2>
|
||||
<p>{{tag.PostCount}} posts</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
{{#equal tag.Sort "popular"}}
|
||||
<a href="?sort=popular"><b>Popular</b></a>
|
||||
<a href="?sort=newest">Newest</a>
|
||||
<a href="?sort=best">Best</a>
|
||||
{{/equal}}
|
||||
{{#equal tag.Sort "newest"}}
|
||||
<a href="?sort=popular">Popular</a>
|
||||
<a href="?sort=newest"><b>Newest</b></a>
|
||||
<a href="?sort=best">Best</a>
|
||||
{{/equal}}
|
||||
{{#equal tag.Sort "best"}}
|
||||
<a href="?sort=popular">Popular</a>
|
||||
<a href="?sort=newest">Newest</a>
|
||||
<a href="?sort=best"><b>Best</b></a>
|
||||
{{/equal}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="posts">
|
||||
{{#each tag.Posts}}
|
||||
{{> partials/post }}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 font-bold">
|
||||
{{#if displayPrev}}
|
||||
<a href="{{channel.RelUrl}}?page={{prevPage}}">Previous page</a>
|
||||
{{/if}}
|
||||
<a href="{{channel.RelUrl}}?page={{nextPage}}">Next page</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
79
views/trending.hbs
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Trending - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
{{> partials/nav }}
|
||||
|
||||
<section class="my-4 w-full flex flex-col items-center">
|
||||
{{> partials/searchBar }}
|
||||
</section>
|
||||
|
||||
<header class="p-4 rounded-xl text-white mb-4 bg-gradient-to-r from-blue-400 to-emerald-400">
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<h2 class="text-2xl font-extrabold">Trending</h2>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between">
|
||||
<div class="flex flex-col">
|
||||
{{#equal section "hot"}}
|
||||
<a href="?section=hot&sort={{sort}}"><b>Hot</b></a>
|
||||
<a href="?section=new&sort={{sort}}">New</a>
|
||||
<a href="?section=top&sort={{sort}}">Top</a>
|
||||
{{/equal}}
|
||||
{{#equal section "new"}}
|
||||
<a href="?section=hot&sort={{sort}}">Hot</a>
|
||||
<a href="?section=new&sort={{sort}}"><b>New</b></a>
|
||||
<a href="?section=top&sort={{sort}}">Top</a>
|
||||
{{/equal}}
|
||||
{{#equal section "top"}}
|
||||
<a href="?section=hot&sort={{sort}}">Hot</a>
|
||||
<a href="?section=new&sort={{sort}}">New</a>
|
||||
<a href="?section=top&sort={{sort}}"><b>Top</b></a>
|
||||
{{/equal}}
|
||||
</div>
|
||||
<hr class="sm:hidden my-2" />
|
||||
<div class="flex flex-col sm:items-end">
|
||||
{{#equal sort "popular"}}
|
||||
<a href="?section={{section}}&sort=popular"><b>Popular</b></a>
|
||||
<a href="?section={{section}}&sort=newest">Newest</a>
|
||||
<a href="?section={{section}}&sort=best">Best</a>
|
||||
{{/equal}}
|
||||
{{#equal sort "newest"}}
|
||||
<a href="?section={{section}}&sort=popular">Popular</a>
|
||||
<a href="?section={{section}}&sort=newest"><b>Newest</b></a>
|
||||
<a href="?section={{section}}&sort=best">Best</a>
|
||||
{{/equal}}
|
||||
{{#equal sort "best"}}
|
||||
<a href="?section={{section}}&sort=popular">Popular</a>
|
||||
<a href="?section={{section}}&sort=newest">Newest</a>
|
||||
<a href="?section={{section}}&sort=best"><b>Best</b></a>
|
||||
{{/equal}}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="posts">
|
||||
{{#each results}}
|
||||
{{> partials/post }}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-4 font-bold">
|
||||
{{#if displayPrev}}
|
||||
<a href="/trending?section={{section}}&sort={{sort}}&page={{prevPage}}">Previous page</a>
|
||||
{{/if}}
|
||||
<p>Page {{page}}</p>
|
||||
<a href="/trending?section={{section}}&sort={{sort}}&page={{nextPage}}">Next page</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{> partials/footer }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||