Compare commits
326 Commits
lambda
...
permissive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b78ed44730 | ||
|
|
c208a55f40 | ||
|
|
4441d25d38 | ||
|
|
975ffa0b9c | ||
|
|
7b1314fae3 | ||
|
|
fd704f53e7 | ||
|
|
bf849e1cbc | ||
|
|
cd4a36c9f7 | ||
|
|
04fbc7f5f4 | ||
|
|
189ebeefde | ||
|
|
3f40c25b04 | ||
|
|
33fa04e98d | ||
|
|
e5b87dc924 | ||
|
|
8cb2524924 | ||
|
|
23b66cba47 | ||
|
|
eabb7d9917 | ||
|
|
3b8ad3f360 | ||
|
|
e8ed026962 | ||
|
|
107a45e58b | ||
|
|
751616d967 | ||
|
|
09222a116f | ||
|
|
42174c052d | ||
|
|
c22365100d | ||
|
|
26edb6a385 | ||
|
|
dc57521bfa | ||
|
|
44ce694b5b | ||
|
|
2cef3880c7 | ||
|
|
061f9b0ebe | ||
|
|
7811070f6d | ||
|
|
248782a236 | ||
|
|
1a0016cec3 | ||
|
|
af919226ed | ||
|
|
3d2b0eeb2a | ||
|
|
2cb5bcebf3 | ||
|
|
4e3022ce1a | ||
|
|
65e4a5f08e | ||
|
|
0f1fbc2401 | ||
|
|
4afb7cefe7 | ||
|
|
20715f53af | ||
|
|
ac9582df0a | ||
|
|
0ce85ed2bd | ||
|
|
09a76779c9 | ||
|
|
337796b9be | ||
|
|
927ea20fad | ||
|
|
7433265991 | ||
|
|
fb82afc7dd | ||
|
|
1579e59dca | ||
|
|
6d528c7d0a | ||
|
|
9a6173a662 | ||
|
|
c366f95f6b | ||
|
|
f5e1669a61 | ||
|
|
dd78541a46 | ||
|
|
29203a1a78 | ||
|
|
13a72701e1 | ||
|
|
252b557450 | ||
|
|
f46ef8b029 | ||
|
|
1376b5da9d | ||
|
|
157407c013 | ||
|
|
8fe3f73568 | ||
|
|
5cce60c3ad | ||
|
|
1ec92f4809 | ||
|
|
7a0c008bba | ||
|
|
d354d6314a | ||
|
|
121ad6b157 | ||
|
|
fc88bfbca5 | ||
|
|
0fea1e46a3 | ||
|
|
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
|
||||
36
.env.example
Normal file
@@ -0,0 +1,36 @@
|
||||
ADDRESS=0.0.0.0
|
||||
PORT=3000
|
||||
FIBER_PREFORK=false
|
||||
IMGUR_CLIENT_ID=546c25a59c58ad7
|
||||
SECURE=true
|
||||
|
||||
# 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
|
||||
5
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
dist
|
||||
.env
|
||||
tmp
|
||||
static/app.css
|
||||
dist/
|
||||
node_modules
|
||||
55
.goreleaser.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- pnpm run build
|
||||
- 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:
|
||||
- formats: tar.gz
|
||||
name_template: >-
|
||||
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
kos:
|
||||
- repositories:
|
||||
- codeberg.org/rimgo/rimgo
|
||||
- codeberg.org/video-prize-ranch/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 pnpm
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install && pnpm run build
|
||||
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"]
|
||||
|
||||
14
Justfile
Normal file
@@ -0,0 +1,14 @@
|
||||
build:
|
||||
pnpm run build
|
||||
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-css:
|
||||
pnpm run watch
|
||||
|
||||
dev:
|
||||
go run github.com/air-verse/air@latest -c .air.toml
|
||||
|
||||
tag-vpr:
|
||||
podman pull codeberg.org/rimgo/rimgo:latest
|
||||
podman image tag codeberg.org/rimgo/rimgo:latest codeberg.org/video-prize-ranch/rimgo:latest
|
||||
podman push codeberg.org/video-prize-ranch/rimgo:latest
|
||||
148
README.md
@@ -1,116 +1,84 @@
|
||||
<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)
|
||||
- [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/redirection/)
|
||||
- [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 84 trackers and 264 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.
|
||||
|
||||
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!
|
||||
|
||||
| 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 | |
|
||||
|
||||
Available at https://rimgo.codeberg.page/ or https://codeberg.org/rimgo/instances
|
||||
|
||||
## 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). Please remember to follow our [Code of Conduct](https://rimgo.codeberg.page/docs/code-of-conduct/)!
|
||||
|
||||
PRs are welcome!
|
||||
## 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.
|
||||
|
||||
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.
|
||||
## Legal notice
|
||||
rimgo does not allow uploads or host any content, media. All content on any rimgo instances is from Imgur™. Imgur is a trademark of Imgur, Inc. Any issues with content on rimgo should be be reported to Imgur. rimgo is not affiliated with Imgur, Inc.
|
||||
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")
|
||||
if err != nil {
|
||||
return types.Album{}, err
|
||||
func (client *Client) FetchPosts(albumID string) (Album, error) {
|
||||
cacheData, found := client.Cache.Get(albumID + "-posts")
|
||||
if found {
|
||||
return cacheData.(Album), nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
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
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
album, err := parseAlbum(data)
|
||||
if err != nil {
|
||||
return Album{}, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
100
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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
|
||||
cacheData, found := client.Cache.Get(galleryID + "-comments")
|
||||
if found {
|
||||
return cacheData.([]Comment), nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.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 []types.Comment{}, err
|
||||
return []Comment{}, nil
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"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) {
|
||||
// Dots are automatically removed on Imgur, so more cache hits
|
||||
tag = strings.ReplaceAll(tag, ".", "")
|
||||
|
||||
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 := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
posts := make([]Submission, 0)
|
||||
data.Get("posts").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
url, _ := url.Parse(strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""))
|
||||
q := url.Query()
|
||||
q.Add("tag", tag+"."+sort+"."+page+"."+key.String())
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
posts = append(posts, Submission{
|
||||
Id: value.Get("id").String(),
|
||||
Title: value.Get("title").String(),
|
||||
Link: url.String(),
|
||||
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
|
||||
},
|
||||
)
|
||||
|
||||
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, 4*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
|
||||
}
|
||||
268
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"))
|
||||
if err != nil {
|
||||
return types.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
|
||||
type User struct {
|
||||
Id int64
|
||||
Bio string
|
||||
Username string
|
||||
Points int64
|
||||
Cover string
|
||||
Avatar string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
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"))
|
||||
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.Submission{}, err
|
||||
return User{}, 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
|
||||
@@ -1,11 +1,15 @@
|
||||
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
|
||||
52
go.mod
@@ -1,38 +1,28 @@
|
||||
module codeberg.org/video-prize-ranch/rimgo
|
||||
module codeberg.org/rimgo/rimgo
|
||||
|
||||
go 1.17
|
||||
go 1.24.0
|
||||
|
||||
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.11.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mailgun/raymond/v2 v2.0.48
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
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/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.31.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
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
||||
162
instances.json
Normal file
@@ -0,0 +1,162 @@
|
||||
[
|
||||
{
|
||||
"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://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://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.fascinated.cc",
|
||||
"countries": [
|
||||
"us"
|
||||
],
|
||||
"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
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.drgns.space",
|
||||
"countries": [
|
||||
"us"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.quantenzitrone.eu",
|
||||
"countries": [
|
||||
"cz"
|
||||
],
|
||||
"cloudflare": false
|
||||
},
|
||||
{
|
||||
"url": "https://rimgo.frylo.net",
|
||||
"countries": [
|
||||
"nl"
|
||||
],
|
||||
"cloudflare": false
|
||||
}
|
||||
]
|
||||
188
main.go
@@ -1,71 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/template/handlebars"
|
||||
"github.com/spf13/viper"
|
||||
"codeberg.org/rimgo/rimgo/pages"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/static"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/views"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var fiberLambda *fiberadaptor.FiberLambda
|
||||
// a handler that returns error if it can't respond
|
||||
type handler func(w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
func init() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yml")
|
||||
viper.AddConfigPath("/etc/rimgu/")
|
||||
viper.AddConfigPath("$HOME/.config/rimgu")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
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()
|
||||
func wrapHandler(h handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
utils.RenderError(w, r, 500, fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
err := h(w, r)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
utils.RenderError(w, r, 500, err.Error())
|
||||
}
|
||||
|
||||
engine := handlebars.NewFileSystem(http.FS(views.GetFiles()), ".hbs")
|
||||
app := fiber.New(fiber.Config{
|
||||
Views: engine,
|
||||
Prefork: viper.GetBool("FIBER_PREFORK"),
|
||||
UnescapePath: true,
|
||||
StreamRequestBody: false,
|
||||
})
|
||||
|
||||
app.Get("/robots.txt", func(c *fiber.Ctx) error {
|
||||
file, _ := static.GetFiles().ReadFile("robots.txt")
|
||||
_, 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("/user/:userID", pages.HandleUser)
|
||||
app.Get("/user/:userID/cover", pages.HandleUserCover)
|
||||
app.Get("/user/:userID/avatar", pages.HandleUserAvatar)
|
||||
app.Get("/gallery/:galleryID", pages.HandleGallery)
|
||||
|
||||
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)
|
||||
envPath := flag.String("c", ".env", "Path to env file")
|
||||
godotenv.Load(*envPath)
|
||||
utils.LoadConfig()
|
||||
|
||||
pages.InitializeApiClient()
|
||||
|
||||
views := views.GetFiles()
|
||||
static := static.GetFiles()
|
||||
render.Initialize(views)
|
||||
|
||||
app := http.NewServeMux()
|
||||
|
||||
app.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static)))
|
||||
app.Handle("GET /robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file, _ := static.Open("robots.txt")
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
}))
|
||||
app.Handle("GET /favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file, _ := static.Open("favicon/favicon.ico")
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
}))
|
||||
|
||||
if os.Getenv("ENV") == "dev" {
|
||||
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderError(w, r, 429)
|
||||
}))
|
||||
app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "/static/img/error-429.png")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderError(w, r, 404)
|
||||
}))
|
||||
app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "/static/img/error-404.png")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
app.Handle("GET /errors/error", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return fmt.Errorf("Test error")
|
||||
}))
|
||||
app.Handle("GET /errors/panic", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
panic("Test error")
|
||||
}))
|
||||
}
|
||||
app.Handle("GET /{$}", wrapHandler(pages.HandleFrontpage))
|
||||
app.Handle("GET /a/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /a/{postID}/embed", wrapHandler(pages.HandleEmbed))
|
||||
app.Handle("GET /t/{tag}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
name, ext := utils.SplitNameExt(r.PathValue("tag"))
|
||||
if ext != "" {
|
||||
r.SetPathValue("tag", name[0:len(name)-1])
|
||||
r.SetPathValue("type", ext)
|
||||
return pages.HandleTagRSS(w, r)
|
||||
}
|
||||
return pages.HandleTag(w, r)
|
||||
}))
|
||||
app.Handle("GET /t/{tag}/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /r/{sub}/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /user/{userID}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
name, ext := utils.SplitNameExt(r.PathValue("userID"))
|
||||
if ext != "" {
|
||||
r.SetPathValue("userID", name[0:len(name)-1])
|
||||
r.SetPathValue("type", ext)
|
||||
return pages.HandleUserRSS(w, r)
|
||||
}
|
||||
return pages.HandleUser(w, r)
|
||||
}))
|
||||
app.Handle("GET /user/{userID}/favorites", wrapHandler(pages.HandleUserFavorites))
|
||||
app.Handle("GET /user/{userID}/comments", wrapHandler(pages.HandleUserComments))
|
||||
app.Handle("GET /user/{userID}/cover", wrapHandler(pages.HandleUserCover))
|
||||
app.Handle("GET /user/{userID}/avatar", wrapHandler(pages.HandleUserAvatar))
|
||||
app.Handle("GET /gallery/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /gallery/{postID}/embed", wrapHandler(pages.HandleEmbed))
|
||||
app.Handle("GET /{component}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
component := r.PathValue("component")
|
||||
switch {
|
||||
case component == "about":
|
||||
return pages.HandleAbout(w, r)
|
||||
case component == "privacy":
|
||||
return pages.HandlePrivacy(w, r)
|
||||
case component == "search":
|
||||
return pages.HandleSearch(w, r)
|
||||
case component == "trending":
|
||||
return pages.HandleTrending(w, r)
|
||||
case strings.HasPrefix(component, "trending."):
|
||||
_, ext := utils.SplitNameExt(component)
|
||||
r.SetPathValue("type", ext)
|
||||
return pages.HandleTrendingRSS(w, r)
|
||||
case strings.HasSuffix(component, ".gifv"):
|
||||
r.SetPathValue("postID", component)
|
||||
return pages.HandleGifv(w, r)
|
||||
case strings.Contains(component, "."):
|
||||
return pages.HandleMedia(w, r)
|
||||
default:
|
||||
r.SetPathValue("postID", component)
|
||||
return pages.HandlePost(w, r)
|
||||
}
|
||||
}))
|
||||
app.Handle("GET /stack/{component}", wrapHandler(pages.HandleMedia))
|
||||
// matches anything with no more specific route
|
||||
app.Handle("GET /", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
err := render.Render(w, "errors/404", nil)
|
||||
return err
|
||||
}))
|
||||
|
||||
addr := utils.Config.Addr + ":" + utils.Config.Port
|
||||
fmt.Println("listening on " + addr)
|
||||
err := http.ListenAndServe(addr, app)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.5",
|
||||
"tailwindcss": "^4.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tailwindcss -i static/tailwind.css -o static/app.css",
|
||||
"watch": "tailwindcss -i static/tailwind.css -o static/app.css --watch"
|
||||
}
|
||||
}
|
||||
21
pages/about.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleAbout(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().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 render.Render(w, "about", map[string]any{
|
||||
"proto": r.Proto,
|
||||
"domain": r.Host,
|
||||
"force_webp": utils.Config.ForceWebp,
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
49
pages/embed.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleEmbed(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().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(r.URL.Path, "/a"):
|
||||
post, err = ApiClient.FetchAlbum(r.PathValue("postID"))
|
||||
case strings.HasPrefix(r.URL.Path, "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(r.PathValue("postID"))
|
||||
default:
|
||||
post, err = ApiClient.FetchMedia(r.PathValue("postID"))
|
||||
}
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return render.Render(w, "embed", map[string]any{
|
||||
"post": post,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleGifv(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().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 render.Render(w, "gifv", map[string]any{
|
||||
"id": r.PathValue("postID"),
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
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(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().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 render.Render(w, "frontpage", map[string]any{
|
||||
"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,42 +1,93 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/video-prize-ranch/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleMedia(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
|
||||
func HandleMedia(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; img-src 'self'")
|
||||
splitName := strings.SplitN(r.PathValue("component"), ".", 2)
|
||||
baseName, extension := splitName[0], splitName[1]
|
||||
if strings.HasPrefix(r.URL.Path, "/stack") {
|
||||
return handleMedia(w, r, "https://i.stack.imgur.com/"+strings.ReplaceAll(baseName, "stack/", "")+"."+extension)
|
||||
} else {
|
||||
return handleMedia(w, r, "https://i.imgur.com/"+baseName+"."+extension)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUserCover(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/cover?maxwidth=2560")
|
||||
};
|
||||
func HandleUserCover(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'")
|
||||
return handleMedia(w, r, "https://imgur.com/user/"+r.PathValue("userID")+"/cover?maxwidth=2560")
|
||||
}
|
||||
|
||||
func HandleUserAvatar(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/avatar")
|
||||
};
|
||||
func HandleUserAvatar(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'")
|
||||
return handleMedia(w, r, "https://imgur.com/user/"+r.PathValue("userID")+"/avatar")
|
||||
}
|
||||
|
||||
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")
|
||||
func handleMedia(w http.ResponseWriter, r *http.Request, url string) error {
|
||||
utils.SetHeaders(w)
|
||||
if !utils.Config.RestrictiveCORS {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
path := r.URL.Path
|
||||
|
||||
res, err := http.Get(url)
|
||||
if utils.Config.ForceWebp &&
|
||||
!strings.HasSuffix(path, ".webp") &&
|
||||
r.Header.Get("Sec-Fetch-Dest") == "image" &&
|
||||
r.URL.Query().Get("no_webp") == "" &&
|
||||
utils.Accepts(r, "image/webp") &&
|
||||
!strings.HasPrefix(path, "/stack") {
|
||||
url = strings.ReplaceAll(url, ".png", ".webp")
|
||||
url = strings.ReplaceAll(url, ".jpg", ".webp")
|
||||
url = strings.ReplaceAll(url, ".jpeg", ".webp")
|
||||
filename := strings.TrimPrefix(path, "/")
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename*": filename}))
|
||||
}
|
||||
|
||||
queryStr := r.URL.Query().Encode()
|
||||
if strings.HasPrefix(path, "/stack") && queryStr != "" {
|
||||
url = url + "?" + queryStr
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
utils.SetReqHeaders(req)
|
||||
|
||||
rng := r.URL.Query().Get("Range")
|
||||
if rng != "" {
|
||||
req.Header.Set("Range", rng)
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", res.Header.Get("Content-Type"));
|
||||
return c.Send(body)
|
||||
if res.StatusCode == 404 || strings.Contains(res.Request.URL.String(), "error/404") {
|
||||
return utils.RenderError(w, r, 404)
|
||||
} else if res.StatusCode == 429 {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Content-Type", res.Header.Get("Content-Type"))
|
||||
w.Header().Set("Content-Length", res.Header.Get("Content-Length"))
|
||||
if res.Header.Get("Content-Range") != "" {
|
||||
w.Header().Set("Content-Range", res.Header.Get("Content-Range"))
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, res.Body)
|
||||
return err
|
||||
}
|
||||
101
pages/post.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
// Cursed function
|
||||
func nextInTag(client *api.Client, tagname, sort, page, I string) string {
|
||||
i, err := strconv.Atoi(I)
|
||||
if err != nil || i < 0 {
|
||||
return ""
|
||||
}
|
||||
tag, err := client.FetchTag(tagname, sort, page)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if i >= len(tag.Posts)-1 {
|
||||
pageNumber, _ := strconv.Atoi(page)
|
||||
tagn, err := client.FetchTag(tagname, sort, strconv.Itoa(pageNumber+1))
|
||||
// Check length - Imgur will not return an error if there are no more posts and you request the next page
|
||||
if err != nil || len(tagn.Posts) < 1 {
|
||||
return ""
|
||||
}
|
||||
return tagn.Posts[0].Link
|
||||
}
|
||||
return tag.Posts[i+1].Link
|
||||
}
|
||||
|
||||
func HandlePost(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
postId := r.PathValue("postID")
|
||||
if strings.Contains(postId, "-") {
|
||||
postId = postId[len(postId)-7:]
|
||||
}
|
||||
|
||||
post, err := api.Album{}, error(nil)
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/a"):
|
||||
post, err = ApiClient.FetchAlbum(postId)
|
||||
case strings.HasPrefix(r.URL.Path, "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(postId)
|
||||
case strings.HasPrefix(r.URL.Path, "/t"):
|
||||
post, err = ApiClient.FetchPosts(postId)
|
||||
default:
|
||||
post, err = ApiClient.FetchMedia(postId)
|
||||
}
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comments := []api.Comment{}
|
||||
if post.SharedWithCommunity {
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
comments, err = ApiClient.FetchComments(postId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
w.Header().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 + "'"
|
||||
}
|
||||
w.Header().Set("Content-Security-Policy", csp)
|
||||
|
||||
var next string
|
||||
tagParam := strings.Split(r.URL.Query().Get("tag"), ".")
|
||||
if len(tagParam) == 4 {
|
||||
tag, sort, page, index := tagParam[0], tagParam[1], tagParam[2], tagParam[3]
|
||||
next = nextInTag(ApiClient, tag, sort, page, index)
|
||||
}
|
||||
|
||||
return render.Render(w, "post", map[string]any{
|
||||
"post": post,
|
||||
"next": next,
|
||||
"comments": comments,
|
||||
"nonce": nonce,
|
||||
})
|
||||
}
|
||||
19
pages/privacy.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandlePrivacy(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
return render.Render(w, "privacy", map[string]any{
|
||||
"config": utils.Config,
|
||||
"version": VersionInfo,
|
||||
})
|
||||
}
|
||||
143
pages/rss.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
func HandleTagRSS(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
|
||||
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), "1")
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
w.WriteHeader(429)
|
||||
_, err := w.Write([]byte("rate limited by imgur"))
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.Display == "" {
|
||||
w.WriteHeader(404)
|
||||
_, err := w.Write([]byte("tag not found"))
|
||||
return err
|
||||
}
|
||||
|
||||
instance := utils.GetInstanceUrl(r)
|
||||
|
||||
feed := &feeds.Feed{
|
||||
Title: tag.Display + " on Imgur",
|
||||
Link: &feeds.Link{Href: instance + "/t/" + r.PathValue("tag")},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
return handleFeed(w, r, instance, feed, tag.Posts)
|
||||
}
|
||||
|
||||
func HandleTrendingRSS(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
|
||||
section := r.URL.Query().Get("section")
|
||||
switch section {
|
||||
case "hot", "new", "top":
|
||||
default:
|
||||
section = "hot"
|
||||
}
|
||||
sort := r.URL.Query().Get("sort")
|
||||
switch sort {
|
||||
case "newest", "best", "popular":
|
||||
default:
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
results, err := ApiClient.FetchTrending(section, sort, "1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance := utils.GetInstanceUrl(r)
|
||||
|
||||
feed := &feeds.Feed{
|
||||
Title: "Trending on Imgur",
|
||||
Link: &feeds.Link{Href: instance + "/trending"},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
return handleFeed(w, r, instance, feed, results)
|
||||
}
|
||||
|
||||
func HandleUserRSS(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
|
||||
user := r.PathValue("userID")
|
||||
|
||||
submissions, err := ApiClient.FetchSubmissions(user, "newest", "1")
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance := utils.GetInstanceUrl(r)
|
||||
|
||||
feed := &feeds.Feed{
|
||||
Title: user + " on Imgur",
|
||||
Link: &feeds.Link{Href: instance + "/user/" + user},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
return handleFeed(w, r, instance, feed, submissions)
|
||||
}
|
||||
|
||||
func handleFeed(w http.ResponseWriter, r *http.Request, instance string, feed *feeds.Feed, posts []api.Submission) error {
|
||||
feed.Items = []*feeds.Item{}
|
||||
|
||||
for _, post := range posts {
|
||||
link := instance + post.Link
|
||||
|
||||
item := &feeds.Item{
|
||||
Title: post.Title,
|
||||
Link: &feeds.Link{Href: link},
|
||||
Description: "<a href=\"" + link + "\"><img width=\"480\" src=\"" + instance + "/" + post.Cover.Id + ".jpeg" + "\"></a>",
|
||||
}
|
||||
|
||||
if post.Cover.Type == "video" {
|
||||
item.Description = "🎞️ Video<br><br>" + item.Description
|
||||
}
|
||||
|
||||
feed.Items = append(feed.Items, item)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension("."+r.PathValue("type")))
|
||||
switch r.PathValue("type") {
|
||||
case "atom":
|
||||
body, err := feed.ToAtom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
case "json":
|
||||
body, err := feed.ToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
case "rss":
|
||||
body, err := feed.ToRss()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("invalid type"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
pages/search.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleSearch(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().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 := r.URL.Query().Get("q")
|
||||
|
||||
if utils.ImgurRe.MatchString(query) {
|
||||
w.Header().Set("Location", utils.ImgurRe.ReplaceAllString(query, ""))
|
||||
w.WriteHeader(302)
|
||||
return nil
|
||||
}
|
||||
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "0"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
results, err := ApiClient.Search(query, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return render.Render(w, "search", map[string]any{
|
||||
"query": query,
|
||||
"results": results,
|
||||
"page": pageNumber,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
44
pages/tag.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleTag(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().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 := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.Display == "" {
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
return render.Render(w, "tag", map[string]any{
|
||||
"tag": tag,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
53
pages/trending.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleTrending(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().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 := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 1
|
||||
}
|
||||
|
||||
section := r.URL.Query().Get("section")
|
||||
switch section {
|
||||
case "hot", "new", "top":
|
||||
default:
|
||||
section = "hot"
|
||||
}
|
||||
sort := r.URL.Query().Get("sort")
|
||||
switch sort {
|
||||
case "newest", "best", "popular":
|
||||
default:
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
results, err := ApiClient.FetchTrending(section, sort, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return render.Render(w, "trending", map[string]any{
|
||||
"results": results,
|
||||
"section": section,
|
||||
"sort": sort,
|
||||
"page": pageNumber,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
140
pages/user.go
@@ -1,42 +1,128 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandleUser(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
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")
|
||||
func HandleUser(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().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 := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "0"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
user, err := ApiClient.FetchUser(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
submissions, err := ApiClient.FetchSubmissions(r.PathValue("userID"), "newest", page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
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{
|
||||
return render.Render(w, "user", map[string]any{
|
||||
"user": user,
|
||||
"submissions": submissions,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUserComments(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().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(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
comments, err := ApiClient.FetchUserComments(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return render.Render(w, "userComments", map[string]any{
|
||||
"user": user,
|
||||
"comments": comments,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().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 := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "0"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
user, err := ApiClient.FetchUser(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
favorites, err := ApiClient.FetchUserFavorites(r.PathValue("userID"), "newest", page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return render.Render(w, "userFavorites", map[string]any{
|
||||
"user": user,
|
||||
"favorites": favorites,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
547
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,547 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@tailwindcss/cli':
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
tailwindcss:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
|
||||
packages:
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.1':
|
||||
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.1':
|
||||
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.1':
|
||||
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher@2.5.1':
|
||||
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
'@tailwindcss/cli@4.1.5':
|
||||
resolution: {integrity: sha512-Kr567rDwDjY1VUnfqh5/+DCpRf4B8lPs5O9flP4kri7n4AM2aubrIxGSh5GN8s+awUKw/U4+6kNlEnZbBNfUeg==}
|
||||
hasBin: true
|
||||
|
||||
'@tailwindcss/node@4.1.5':
|
||||
resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.5':
|
||||
resolution: {integrity: sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.5':
|
||||
resolution: {integrity: sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.5':
|
||||
resolution: {integrity: sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.5':
|
||||
resolution: {integrity: sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5':
|
||||
resolution: {integrity: sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.5':
|
||||
resolution: {integrity: sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.5':
|
||||
resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.5':
|
||||
resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.5':
|
||||
resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.5':
|
||||
resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@tybys/wasm-util'
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.5':
|
||||
resolution: {integrity: sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.5':
|
||||
resolution: {integrity: sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.1.5':
|
||||
resolution: {integrity: sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
detect-libc@2.0.4:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
jiti@2.4.2:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.29.2:
|
||||
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
tailwindcss@4.1.5:
|
||||
resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==}
|
||||
|
||||
tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher@2.5.1':
|
||||
dependencies:
|
||||
detect-libc: 1.0.3
|
||||
is-glob: 4.0.3
|
||||
micromatch: 4.0.8
|
||||
node-addon-api: 7.1.1
|
||||
optionalDependencies:
|
||||
'@parcel/watcher-android-arm64': 2.5.1
|
||||
'@parcel/watcher-darwin-arm64': 2.5.1
|
||||
'@parcel/watcher-darwin-x64': 2.5.1
|
||||
'@parcel/watcher-freebsd-x64': 2.5.1
|
||||
'@parcel/watcher-linux-arm-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-arm-musl': 2.5.1
|
||||
'@parcel/watcher-linux-arm64-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-arm64-musl': 2.5.1
|
||||
'@parcel/watcher-linux-x64-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-x64-musl': 2.5.1
|
||||
'@parcel/watcher-win32-arm64': 2.5.1
|
||||
'@parcel/watcher-win32-ia32': 2.5.1
|
||||
'@parcel/watcher-win32-x64': 2.5.1
|
||||
|
||||
'@tailwindcss/cli@4.1.5':
|
||||
dependencies:
|
||||
'@parcel/watcher': 2.5.1
|
||||
'@tailwindcss/node': 4.1.5
|
||||
'@tailwindcss/oxide': 4.1.5
|
||||
enhanced-resolve: 5.18.1
|
||||
mri: 1.2.0
|
||||
picocolors: 1.1.1
|
||||
tailwindcss: 4.1.5
|
||||
|
||||
'@tailwindcss/node@4.1.5':
|
||||
dependencies:
|
||||
enhanced-resolve: 5.18.1
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.2
|
||||
tailwindcss: 4.1.5
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.1.5':
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.1.5
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.1.5
|
||||
'@tailwindcss/oxide-darwin-x64': 4.1.5
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.1.5
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.5
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.5
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.1.5
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.1.5
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.1.5
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.5
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.5
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.5
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
detect-libc@1.0.3: {}
|
||||
|
||||
detect-libc@2.0.4: {}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.29.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
optionalDependencies:
|
||||
lightningcss-darwin-arm64: 1.29.2
|
||||
lightningcss-darwin-x64: 1.29.2
|
||||
lightningcss-freebsd-x64: 1.29.2
|
||||
lightningcss-linux-arm-gnueabihf: 1.29.2
|
||||
lightningcss-linux-arm64-gnu: 1.29.2
|
||||
lightningcss-linux-arm64-musl: 1.29.2
|
||||
lightningcss-linux-x64-gnu: 1.29.2
|
||||
lightningcss-linux-x64-musl: 1.29.2
|
||||
lightningcss-win32-arm64-msvc: 1.29.2
|
||||
lightningcss-win32-x64-msvc: 1.29.2
|
||||
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
tailwindcss@4.1.5: {}
|
||||
|
||||
tapable@2.2.1: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
17
render/helpers.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package render
|
||||
|
||||
import "github.com/mailgun/raymond/v2"
|
||||
|
||||
func (r *renderer) registerHelpers() {
|
||||
funcmap := map[string]any{
|
||||
"noteq": noteq,
|
||||
}
|
||||
raymond.RegisterHelpers(funcmap)
|
||||
}
|
||||
|
||||
func noteq(a, b any, options *raymond.Options) any {
|
||||
if raymond.Str(a) != raymond.Str(b) {
|
||||
return options.Fn()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
76
render/render.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// stolen from gofiber/template but simpler
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mailgun/raymond/v2"
|
||||
)
|
||||
|
||||
var Renderer *renderer
|
||||
|
||||
func Render(out io.Writer, name string, bind map[string]any) error {
|
||||
return Renderer.Render(out, name, bind)
|
||||
}
|
||||
|
||||
type renderer struct {
|
||||
templates map[string]*raymond.Template
|
||||
}
|
||||
|
||||
const ext = ".hbs"
|
||||
|
||||
func Initialize(views fs.FS) {
|
||||
r := new(renderer)
|
||||
r.templates = make(map[string]*raymond.Template)
|
||||
r.registerHelpers()
|
||||
fs.WalkDir(views, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
name, hasExt := strings.CutSuffix(path, ext)
|
||||
if !hasExt {
|
||||
return nil
|
||||
}
|
||||
path = filepath.ToSlash(path)
|
||||
file, err := views.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
buf, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpl, err := raymond.Parse(string(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.templates[name] = tmpl
|
||||
return nil
|
||||
})
|
||||
for j := range r.templates {
|
||||
for n, template := range r.templates {
|
||||
r.templates[j].RegisterPartialTemplate(n, template)
|
||||
}
|
||||
}
|
||||
Renderer = r
|
||||
}
|
||||
|
||||
func (r *renderer) Render(out io.Writer, name string, bind map[string]any) error {
|
||||
tmpl := r.templates[name]
|
||||
if tmpl == nil {
|
||||
return fmt.Errorf("render: template %s does not exist", name)
|
||||
}
|
||||
parsed, err := tmpl.Exec(bind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render: %w", err)
|
||||
}
|
||||
if _, err = out.Write([]byte(parsed)); err != nil {
|
||||
return fmt.Errorf("render: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -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 |
20
static/favicon/site.webmanifest
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "rimgo",
|
||||
"short_name": "rimgo",
|
||||
"start_url": "/",
|
||||
"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/PhRss.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="M106.91 149.09A71.53 71.53 0 0 1 128 200a8 8 0 0 1-16 0a56 56 0 0 0-56-56a8 8 0 0 1 0-16a71.53 71.53 0 0 1 50.91 21.09M56 80a8 8 0 0 0 0 16a104 104 0 0 1 104 104a8 8 0 0 0 16 0A120 120 0 0 0 56 80m118.79 1.21A166.9 166.9 0 0 0 56 32a8 8 0 0 0 0 16a151 151 0 0 1 107.48 44.52A151 151 0 0 1 208 200a8 8 0 0 0 16 0a166.9 166.9 0 0 0-49.21-118.79M60 184a12 12 0 1 0 12 12a12 12 0 0 0-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 508 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 |
BIN
static/img/error-404.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/img/error-429.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/img/error-generic.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
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 |
72
static/tailwind.css
Normal file
@@ -0,0 +1,72 @@
|
||||
@import "tailwindcss" source("../views");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
22
utils/accepts.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Accepts(r *http.Request, format string) bool {
|
||||
format = strings.ToLower(format)
|
||||
group := strings.Split(format, "/")[0] + "/*"
|
||||
header := r.Header.Get("Accept")
|
||||
if header == "" {
|
||||
return false
|
||||
}
|
||||
for _, mime := range strings.Split(header, ",") {
|
||||
mime = strings.ToLower(strings.TrimSpace(strings.SplitN(mime, ";", 2)[0]))
|
||||
if mime == "*/*" || mime == format || mime == group {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
58
utils/config.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Port string
|
||||
Addr string
|
||||
ImgurId string
|
||||
ProtocolDetection bool
|
||||
Secure bool
|
||||
ForceWebp bool
|
||||
RestrictiveCORS bool
|
||||
ImageCache bool
|
||||
CleanupInterval time.Duration
|
||||
CacheDir string
|
||||
Privacy map[string]interface{}
|
||||
}
|
||||
|
||||
var Config config
|
||||
|
||||
func envString(name, def string) string {
|
||||
env := os.Getenv(name)
|
||||
if env != "" {
|
||||
return env
|
||||
}
|
||||
return def
|
||||
}
|
||||
func envBool(name string) bool {
|
||||
return os.Getenv(name) == "true" || os.Getenv(name) == "1"
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
Config = config{
|
||||
Port: envString("PORT", "3000"),
|
||||
Addr: envString("ADDR", "0.0.0.0"),
|
||||
ImgurId: envString("IMGUR_CLIENT_ID", "546c25a59c58ad7"),
|
||||
ProtocolDetection: envBool("PROTOCOL_DETECTION"),
|
||||
Secure: envBool("SECURE"),
|
||||
ForceWebp: envBool("FORCE_WEBP"),
|
||||
RestrictiveCORS: envBool("RESTRICTIVE_CORS"),
|
||||
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": envBool("PRIVACY_CLOUDFLARE"),
|
||||
"not_collected": envBool("PRIVACY_NOT_COLLECTED"),
|
||||
"ip": envBool("PRIVACY_IP"),
|
||||
"url": envBool("PRIVACY_URL"),
|
||||
"device": envBool("PRIVACY_DEVICE"),
|
||||
"diagnostics": envBool("PRIVACY_DIAGNOSTICS"),
|
||||
},
|
||||
}
|
||||
}
|
||||
43
utils/error.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/static"
|
||||
)
|
||||
|
||||
func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string) (err error) {
|
||||
if len(str) != 1 {
|
||||
str = []string{""}
|
||||
}
|
||||
codeStr := "generic"
|
||||
if code == 0 {
|
||||
code = 500
|
||||
}
|
||||
if code != 500 {
|
||||
codeStr = strconv.Itoa(code)
|
||||
}
|
||||
if !Accepts(r, "text/html") && r.PathValue("extension") != "" {
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.WriteHeader(code)
|
||||
file, _ := static.GetFiles().Open("img/error-" + codeStr + ".png")
|
||||
defer file.Close()
|
||||
_, err = io.Copy(w, file)
|
||||
|
||||
} else {
|
||||
w.WriteHeader(code)
|
||||
err = render.Render(w, "errors/"+codeStr, map[string]any{
|
||||
"path": r.URL.Path,
|
||||
"err": str[0],
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
// don't panic or return error, it will loop
|
||||
fmt.Println("error in RenderError: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
21
utils/getUrl.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import "net/http"
|
||||
|
||||
func GetInstanceProtocol(r *http.Request) string {
|
||||
proto := "https"
|
||||
if !Config.Secure {
|
||||
proto = "http"
|
||||
}
|
||||
if Config.ProtocolDetection {
|
||||
xproto := r.Header.Get("X-Forwarded-Proto")
|
||||
if xproto != "" {
|
||||
proto = xproto
|
||||
}
|
||||
}
|
||||
return proto
|
||||
}
|
||||
|
||||
func GetInstanceUrl(r *http.Request) string {
|
||||
return GetInstanceProtocol(r) + "://" + r.Host
|
||||
}
|
||||
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,26 @@
|
||||
package utils
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
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=()")
|
||||
func SetHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31557600")
|
||||
w.Header().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")
|
||||
}
|
||||
16
utils/splitNameExt.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
func SplitNameExt(path string) (name, ext string) {
|
||||
name, ext = path, ""
|
||||
for range 5 {
|
||||
if len(name) == 0 || name[len(name)-1] == '.' || name[len(name)-1] == '/' {
|
||||
break
|
||||
}
|
||||
name = name[:len(name)-1]
|
||||
ext = path[len(name):]
|
||||
}
|
||||
if len(name) == 0 || name[len(name)-1] != '.' {
|
||||
return path, ""
|
||||
}
|
||||
return
|
||||
}
|
||||
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/generic.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">Legal notice</h3>
|
||||
<p>rimgo does not allow uploads or host any content, media. All content on any rimgo instances is from Imgur™. Imgur is a trademark of Imgur, Inc. Any issues with content on rimgo should be be reported to Imgur. rimgo is not affiliated with Imgur, Inc.</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="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>
|
||||