9 Commits

Author SHA1 Message Date
video-prize-ranch
55e96df4df Fix CSP 2022-01-28 19:07:44 -05:00
video-prize-ranch
d0e7acb273 Switch to S3 2022-01-28 18:52:45 -05:00
video-prize-ranch
5bd4310f09 Switch to S3 static assets 2022-01-28 18:51:15 -05:00
video-prize-ranch
118b467eb9 Remove static files 2022-01-28 18:46:02 -05:00
video-prize-ranch
e3488c3527 Use S3 and CloudFront for static files 2022-01-28 18:44:36 -05:00
video-prize-ranch
be24ab2342 Fix CSP for cloudfront media 2022-01-28 18:27:38 -05:00
video-prize-ranch
668df16fd2 Use CloudFront for media to avoid payload limits 2022-01-28 17:55:44 -05:00
video-prize-ranch
65e5711766 Disable media streaming 2022-01-28 16:40:21 -05:00
video-prize-ranch
cb99714a47 Lambda 2022-01-28 15:50:37 -05:00
103 changed files with 1808 additions and 3227 deletions

View File

@@ -1,42 +0,0 @@
# 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

View File

@@ -1,3 +1,3 @@
.env node_modules
tmp samples
static/app.css dist

View File

@@ -1,35 +0,0 @@
ADDRESS=0.0.0.0
PORT=3000
FIBER_PREFORK=false
IMGUR_CLIENT_ID=546c25a59c58ad7
# Instance privacy
# For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy
# Required to be on the instance list.
# Link to a privacy policy (optional)
PRIVACY_POLICY=
# Explain how this data is used/why it is collected (optional)
PRIVACY_MESSAGE=
# Country where instance is located. Leave blank if running on Tor without clearnet.
PRIVACY_COUNTRY=
# Hosting provider or ISP name. Leave blank if running on Tor without clearnet.
PRIVACY_PROVIDER=
# Set to true if you use Cloudflare (using Cloudflare only as DNS (gray cloud icon), set to false)
PRIVACY_CLOUDFLARE=false
# These settings are for NGINX without data collection disabled.
# Instructions for other reverse proxies and to disable data collection:
# https://codeberg.org/librarian/librarian/wiki/Instance-privacy
# Set to true if no data is collected. (overrides all other options)
PRIVACY_NOT_COLLECTED=false
# IP address
PRIVACY_IP=true
# Request URL
PRIVACY_URL=true
# Device Type (User agent)
PRIVACY_DEVICE=true
# If the data collection is only collected when there is an error and only stored for
# a short amount of time while diagnosing an issue.
PRIVACY_DIAGNOSTICS=false

6
.gitignore vendored
View File

@@ -1,4 +1,2 @@
.env dist
tmp node_modules
static/app.css
dist/

View File

@@ -1,52 +0,0 @@
before:
hooks:
- tailwindcss -i static/tailwind.css -o static/app.css -m
- go mod tidy
project_name: rimgo
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
ldflags:
- -X codeberg.org/rimgo/rimgo/pages.VersionInfo={{.Version}}
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
kos:
- repository: codeberg.org/rimgo/rimgo
tags:
- '{{.Version}}'
- latest
bare: true
preserve_import_paths: false
platforms:
- linux/amd64
- linux/arm64
sbom: none
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
release:
gitea:
owner: rimgo
name: rimgo
name_template: "{{.ProjectName}} v{{.Version}}"
disable: false
mode: append
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

View File

@@ -1,21 +1,14 @@
FROM --platform=$BUILDPLATFORM golang:alpine AS build FROM alpine as build
# install build tools
ARG TARGETARCH RUN apk add go git
RUN go env -w GOPROXY=direct
WORKDIR /src # cache dependencies
RUN apk --no-cache add ca-certificates git nodejs npm ADD go.mod go.sum ./
COPY . .
RUN npx tailwindcss -i static/tailwind.css -o static/app.css -m
RUN go mod download RUN go mod download
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)" # build
ADD . .
FROM scratch as bin RUN go build -o /main
# copy artifacts to a clean image
WORKDIR /app FROM alpine
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /main /main
COPY --from=build /src/rimgo . ENTRYPOINT [ "/main" ]
EXPOSE 3000
CMD ["/app/rimgo"]

View File

@@ -1,7 +0,0 @@
build:
tailwindcss -i static/tailwind.css -o static/app.css -m
CGO_ENABLED=0 go build -o rimgo -ldflags "-X codeberg.org/rimgo/rimgo/pages.VersionInfo=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)"
dev:
tailwindcss -i static/tailwind.css -o static/app.css -m -w &
go run github.com/cosmtrek/air@latest -c .air.toml

195
README.md
View File

@@ -1,133 +1,116 @@
<img alt="" src="https://codeberg.org/rimgo/rimgo/raw/branch/main/static/img/rimgo.svg" width="96" height="96" /> <img src="https://codeberg.org/video-prize-ranch/rimgo/raw/branch/main/static/img/rimgo.svg" width="96" height="96" />
# rimgo # rimgo
An alternative frontend for Imgur. Originally based on [rimgu](https://codeberg.org/3np/rimgu). An alternative frontend for Imgur. Based on [rimgu](https://codeberg.org/3np/rimgu) and rewritten in Go.
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"> <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" height="20px"> <img alt="License: AGPLv3" src="https://shields.io/badge/License-AGPL%20v3-blue.svg">
</a>
<a href="https://matrix.to/#/#rimgo:nitro.chat">
<img alt="Matrix" src="https://img.shields.io/badge/chat-matrix-blue" height="20px">
</a> </a>
## Table of Contents 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.
- [Features](#features)
- [Comparison](#comparison)
- [Speed](#speed)
- [Privacy](#privacy)
- [Usage](#usage)
- [Instances](#instances)
- [Clearnet](#clearnet)
- [Tor](#tor)
- [Contributing](#contributing)
- [License](#license)
### Documentation
Our new documentation is now available at [https://rimgo.codeberg.page/docs/](https://rimgo.codeberg.page/docs/)!
- [Install](https://rimgo.codeberg.page/docs/getting-started/install/)
- [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/)
- [Redirection](https://rimgo.codeberg.page/docs/usage/configuration/)
- [Instance privacy](https://rimgo.codeberg.page/docs/usage/instance-privacy/)
## Features ## Features
- Lightweight
- No JavaScript
- No ads or tracking
- No sign up or app install prompts
- Bandwidth efficient - automatically uses newer image formats (if enabled)
## Comparison - [x] URL-compatible with i.imgur.com - just replace the domain in the URL
Comparing rimgo to Imgur. - [x] Images and videos (gifv, mp4)
- [ ] Galleries with comments
- [x] Albums
- [ ] User page
- [ ] Tag page
### Speed This is currently very early stage software. Some things left to implement (contributions welcome!):
Tested using [Google PageSpeed Insights](https://pagespeed.web.dev/).
| | [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) | - [x] Streaming (currently media is downloaded in full in rimgu before it's returned)
| ------------------- | ------- | --------- | - [ ] Localization/internationalization
| Performance | 91 | 28 | - [x] Pretty CSS styling (responsive?)
| Request count | 29 | 340 | - [ ] Support for other popular image sites
| Resource Size | 218 KiB | 2,542 KiB | - [ ] Filtering and exploration on user/tags pages
| Time to Interactive | 1.6s | 23.8s | - [ ] Responsive scaling of videos on user/tags pages
- [x] Logo
### Privacy Things that are considered out of scope:
Imgur collects information about your device and uses tracking cookies for advertising, this is mentioned in their [privacy policy](https://imgur.com/privacy/). [Blacklight](https://themarkup.org/blacklight) found 31 trackers and 87 third-party cookies.
See what cookies and trackers Imgur uses and where your data gets sent: https://themarkup.org/blacklight?url=imgur.com * 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.
## Usage * Anything requiring JavaScript or the client directly interacting with upstream servers.
Replace imgur.com or i.imgur.com with the instance domain. For i.stack.imgur.com, replace i.stack.imgur.com with the instance domain and add stack/ before the media ID. You can use a browser extension to do this [automatically](#automatically-redirect-links).
Imgur: `https://imgur.com/gallery/j2sOQkJ` -> `https://rimgo.bcow.xyz/gallery/j2sOQkJ`
Stack Overflow: `https://i.stack.imgur.com/KnO3v.jpg?s=64&g=1` -> `https://rimgo.bcow.xyz/stack/KnO3v.jpg?s=64&g=1`
To automatically redirect Imgur links, see [Redirection](https://rimgo.codeberg.page/docs/usage/redirection/).
## Instances ## Instances
Open an issue to have your instance listed here! See the rules for the instance list [here](https://rimgo.codeberg.page/docs/usage/instance-list-rules/).
> For more details on instance privacy, see https://rimgo.codeberg.page/docs/usage/instance-privacy/ Open an issue to have your instance listed here!
### Clearnet | 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 | |
| URL | Country | Provider | Privacy | Notes |
| :------------------------------------------------------------ | :----------- | :----------------------- | :-------------------- | :---- |
| [rimgo.pussthecat.org](https://rimgo.pussthecat.org) | 🇩🇪 DE | Hetzner | ⚠️ Data collected | |
| [rimgo.totaldarkness.net](https://rimgo.totaldarkness.net) | 🇨🇦 CA | Vultr | ✅ Data not collected | |
| [rimgo.bus-hit.me](https://rimgo.bus-hit.me) | 🇨🇦 CA | Oracle | ⚠️ Data collected | |
| [imgur.artemislena.eu](https://imgur.artemislena.eu) | 🇩🇪 DE | Vodafone Deutschland | ✅ Data not collected | Self-hosted, provider is ISP |
| [rimgo.vern.cc](https://rimgo.vern.cc) | 🇺🇸 US | OVHCloud | ✅ Data not collected | [Edited theme](https://git.vern.cc/root/modifications/src/branch/master/rimgo) |
| [rim.odyssey346.dev](https://rim.odyssey346.dev/) | 🇫🇷️ FR | Trolling Solutions (OVH) | ✅ Data not collected | |
| [i.habedieeh.re](https://i.habedieeh.re/) | 🇨🇦️ CA | Oracle Cloud | ✅ Data not collected | |
| [rimgo.hostux.net](https://rimgo.hostux.net/) | 🇫🇷️ FR | Gandi | ⚠️ Data collected | |
| [ri.zzls.xyz](https://ri.zzls.xyz/) | 🇨🇱 CL | TELEFÓNICA CHILE | ✅ Data not collected | Self-hosted, provider is ISP |
| [rimgo.lunar.icu](https://rimgo.marcopisco.com/) | 🇩🇪 DE | Cloudflare | ✅ Data not collected | |
| [imgur.010032.xyz](https://imgur.010032.xyz/) | 🇰🇷 KR | Oracle Cloud | ✅ Data not collected | |
| [rimgo.kling.gg](https://rimgo.kling.gg/) | 🇳🇱 NL | RamNode | ✅ Data not collected | |
| [i.01r.xyz](https://i.01r.xyz/) | 🇺🇸 US | Cloudflare | ✅ Data not collected | |
| [rimgo.projectsegfau.lt](https://rimgo.projectsegfau.lt/) | 🇫🇷 FR, 🇺🇸 US, 🇮🇳 IN | See below | ✅ Data not collected | |
| [rimgo.eu.projectsegfau.lt](https://rimgo.eu.projectsegfau.lt/) | 🇫🇷 FR | Orange S.A. | ✅ Data not collected | |
| [rimgo.us.projectsegfau.lt](https://rimgo.us.projectsegfau.lt/) | 🇺🇸 US | Racknerd | ✅ Data not collected | |
| [rimgo.in.projectsegfau.lt](https://rimgo.in.projectsegfau.lt/) | 🇮🇳 IN | Airtel | ✅ Data not collected | |
| [rimgo.whateveritworks.org](https://rimgo.whateveritworks.org/) | 🇩🇪 DE | Cloudflare | ✅ Data not collected | |
| [rimgo.nohost.network](https://rimgo.nohost.network/) | 🇲🇽 MX | Telmex | ✅ Data not collected | |
| [rimgo.catsarch.com](https://rimgo.catsarch.com/) | 🇺🇸 US | Comcast | ✅ Data not collected | Self-hosted, provider is ISP |
| [rimgo.frontendfriendly.xyz](https://rimgo.frontendfriendly.xyz/) | 🇩🇪 DE | Hetzner | ⚠️ Data collected | |
### Tor
| URL | Privacy | Notes |
| :-- | :------ | :----------------------- |
| [rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | ✅ Data not collected | Onion of rimgo.vern.cc |
| [imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion](http://imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion) | ✅ Data not collected | Onion of imgur.artemislena.eu |
| [rim.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd.onion](http://rim.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd.onion) | ⚠️ Data collected | |
| [tdp6uqjtmok723suum5ms3jbquht6d7dssug4cgcxhfniatb25gcipad.onion](http://tdp6uqjtmok723suum5ms3jbquht6d7dssug4cgcxhfniatb25gcipad.onion) | ✅ Data not collected | Onion of rimgo.privacytools.io |
| [i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion](http://i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion/) | ✅ Data not collected | Onion of i.habedieeh.re |
| [rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion](http://rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion/) | ✅ Data not collected | Onion of ri.zzls.xyz |
| [tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion/](http://tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion/) | ✅ Data not collected | Onion of rimgo.kling.gg |
| [rimgo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion](http://rimgo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/) | ✅ Data not collected | Onion of rimgo.eu.projectsegfau.lt |
### I2P
| URL | Privacy | Notes |
| :-- | :------ | :----------------------- |
| [rimgo.i2p](http://rimgo.i2p) | ✅ Data not collected | i.habedieeh.re on I2P |
| [rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p](http://rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p) | ✅ Data not collected | Same as rimgo.i2p |
| [rimgo.zzls.i2p](http://rimgo.zzls.i2p) | ✅ Data not collected | ri.zzls.xyz on I2P |
| [p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p](http://p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p) | ✅ Data not collected | Same as rimgo.zzls.i2p |
| [ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p](http://ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p) | ✅ Data not collected | rimgo.kling.gg on I2P |
## Install ## 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 ## Configuration
See [Configuration](https://rimgo.codeberg.page/docs/usage/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 |
## Contributing ## Contributing
Pull requests are welcome! If you have any questions or bug reports, open an [issue](https://codeberg.org/rimgo/rimgo/issues/new).
## License PRs are welcome!
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.

View File

@@ -1,117 +1,75 @@
package api package api
import ( import (
"io"
"net/http"
"strings" "strings"
"time"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/video-prize-ranch/rimgo/types"
"github.com/microcosm-cc/bluemonday" "codeberg.org/video-prize-ranch/rimgo/utils"
"github.com/spf13/viper"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
type Album struct { func FetchAlbum(albumID string) (types.Album, error) {
Id string // https://api.imgur.com/post/v1/albums/zk7mdKH?client_id=${CLIENT_ID}&include=media%2Caccount
Title string
Views int64
Upvotes int64
Downvotes int64
SharedWithCommunity bool
CreatedAt string
UpdatedAt string
Comments int64
User User
Media []Media
Tags []Tag
}
type Media struct { res, err := http.Get("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID") + "&include=media%2Caccount")
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 { if err != nil {
return Album{}, err return types.Album{}, err
} }
album, err := parseAlbum(data) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return Album{}, err 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)
} }
client.Cache.Set(albumID+"-album", album, 1*time.Hour)
return album, err return album, err
} }
func (client *Client) FetchPosts(albumID string) (Album, error) { func FetchPosts(albumID string) (types.Album, error) {
cacheData, found := client.Cache.Get(albumID + "-posts") res, err := http.Get("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID") + "&include=media%2Caccount")
if found {
return cacheData.(Album), nil
}
data, err := utils.GetJSON("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount%2Ctags")
if err != nil { if err != nil {
return Album{}, err return types.Album{}, err
} }
album, err := parseAlbum(data) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return Album{}, err return types.Album{}, err
} }
client.Cache.Set(albumID+"-posts", album, 1*time.Hour) data := gjson.Parse(string(body))
return album, nil
return ParseAlbum(data)
} }
func (client *Client) FetchMedia(mediaID string) (Album, error) { func ParseAlbum(data gjson.Result) (types.Album, error) {
cacheData, found := client.Cache.Get(mediaID + "-media") media := make([]types.Media, 0)
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( data.Get("media").ForEach(
func(key gjson.Result, value gjson.Result) bool { func(key gjson.Result, value gjson.Result) bool {
url := value.Get("url").String() url := value.Get("url").String()
url = strings.ReplaceAll(url, "https://i.imgur.com", "") url = strings.ReplaceAll(url, "https://i.imgur.com", "")
description := value.Get("metadata.description").String() if strings.HasSuffix(url, "mp4") || viper.GetBool("CF_ALL_MEDIA") {
description = strings.ReplaceAll(description, "\n", "<br>") url = viper.GetString("CF_MEDIA_DISTRIBUTION") + url
description = bluemonday.UGCPolicy().Sanitize(description) }
media = append(media, Media{ media = append(media, types.Media{
Id: value.Get("id").String(), Id: value.Get("id").String(),
Name: value.Get("name").String(), Name: value.Get("name").String(),
MimeType: value.Get("mime_type").String(), MimeType: value.Get("mime_type").String(),
Type: value.Get("type").String(), Type: value.Get("type").String(),
Title: value.Get("metadata.title").String(), Title: value.Get("metadata.title").String(),
Description: description, Description: value.Get("metadata.description").String(),
Url: url, Url: url,
}) })
@@ -119,25 +77,12 @@ func parseAlbum(data gjson.Result) (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()) createdAt, err := utils.FormatDate(data.Get("created_at").String())
if err != nil { if err != nil {
return Album{}, err return types.Album{}, err
} }
album := Album{ return types.Album{
Id: data.Get("id").String(), Id: data.Get("id").String(),
Title: data.Get("title").String(), Title: data.Get("title").String(),
SharedWithCommunity: data.Get("shared_with_community").Bool(), SharedWithCommunity: data.Get("shared_with_community").Bool(),
@@ -147,17 +92,5 @@ func parseAlbum(data gjson.Result) (Album, error) {
Comments: data.Get("comment_count").Int(), Comments: data.Get("comment_count").Int(),
CreatedAt: createdAt, CreatedAt: createdAt,
Media: media, Media: media,
Tags: tags, }, nil
}
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
} }

View File

@@ -1,21 +0,0 @@
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
}

View File

@@ -1,54 +1,43 @@
package api package api
import ( import (
"regexp" "io"
"net/http"
"strings" "strings"
"sync" "sync"
"time" "time"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/video-prize-ranch/rimgo/types"
"codeberg.org/video-prize-ranch/rimgo/utils"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday" "github.com/spf13/viper"
"github.com/patrickmn/go-cache"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"gitlab.com/golang-commonmark/linkify"
) )
type Comment struct { func FetchComments(galleryID string) ([]types.Comment, error) {
Comments []Comment // https://api.imgur.com/comment/v1/comments?client_id=546c25a59c58ad7&filter[post]=eq:g1bk7CB&include=account&per_page=30&sort=best
User User
Post Submission
Id string
Comment string
Upvotes int64
Downvotes int64
Platform string
CreatedAt string
RelTime string
UpdatedAt string
DeletedAt string
}
func (client *Client) FetchComments(galleryID string) ([]Comment, error) { 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")
cacheData, found := client.Cache.Get(galleryID + "-comments")
if found {
return cacheData.([]Comment), nil
}
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 { if err != nil {
return []Comment{}, nil return []types.Comment{}, err
} }
body, err := io.ReadAll(res.Body)
if err != nil {
return []types.Comment{}, err
}
data := gjson.Parse(string(body))
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
comments := make([]Comment, 0) comments := make([]types.Comment, 0)
data.Get("data").ForEach( data.Get("data").ForEach(
func(key, value gjson.Result) bool { func(key, value gjson.Result) bool {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
comments = append(comments, parseComment(value)) comments = append(comments, ParseComment(value))
}() }()
return true return true
@@ -56,34 +45,29 @@ func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
) )
wg.Wait() wg.Wait()
client.Cache.Set(galleryID+"-comments", comments, cache.DefaultExpiration)
return comments, nil return comments, nil
} }
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`) func ParseComment(data gjson.Result) types.Comment {
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()) createdTime, _ := time.Parse("2006-01-02T15:04:05Z", data.Get("created_at").String())
createdAt := createdTime.Format("January 2, 2006 3:04 PM") createdAt := createdTime.Format("January 2, 2006 3:04 PM")
updatedAt, _ := utils.FormatDate(data.Get("updated_at").String()) updatedAt, _ := utils.FormatDate(data.Get("updated_at").String())
deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String()) deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String())
userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "") 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{} wg := sync.WaitGroup{}
comments := make([]Comment, 0) comments := make([]types.Comment, 0)
data.Get("comments").ForEach( data.Get("comments").ForEach(
func(key, value gjson.Result) bool { func(key, value gjson.Result) bool {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
comments = append(comments, parseComment(value)) comments = append(comments, ParseComment(value))
}() }()
return true return true
@@ -91,49 +75,15 @@ func parseComment(data gjson.Result) Comment {
) )
wg.Wait() wg.Wait()
comment := data.Get("comment").String() return types.Comment{
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, Comments: comments,
User: User{ User: types.User{
Id: data.Get("account.id").Int(), Id: data.Get("account.id").Int(),
Username: data.Get("account.username").String(), Username: data.Get("account.username").String(),
Avatar: userAvatar, Avatar: userAvatar,
}, },
Post: parseSubmission(data.Get("post")),
Id: data.Get("id").String(), Id: data.Get("id").String(),
Comment: comment, Comment: data.Get("comment").String(),
Upvotes: data.Get("upvote_count").Int(), Upvotes: data.Get("upvote_count").Int(),
Downvotes: data.Get("downvote_count").Int(), Downvotes: data.Get("downvote_count").Int(),
Platform: data.Get("platform").String(), Platform: data.Get("platform").String(),

19
api/f.ts Normal file
View File

@@ -0,0 +1,19 @@
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 */
}

View File

@@ -1,72 +0,0 @@
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
}

View File

@@ -1,110 +0,0 @@
package api
import (
"io/ioutil"
"net/http"
"strings"
"sync"
"github.com/patrickmn/go-cache"
"github.com/tidwall/gjson"
)
type Tag struct {
Tag string
Display string
Sort string
PostCount int64
Posts []Submission
Background string
BackgroundId string
}
func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error) {
cacheData, found := client.Cache.Get(tag + sort + page + "-tag")
if found {
return cacheData.(Tag), nil
}
req, err := http.NewRequest("GET", "https://api.imgur.com/post/v1/posts/t/"+tag, nil)
if err != nil {
return Tag{}, err
}
q := req.URL.Query()
q.Add("client_id", client.ClientID)
q.Add("include", "cover")
q.Add("page", page)
switch sort {
case "newest":
q.Add("filter[window]", "week")
q.Add("sort", "-time")
case "best":
q.Add("filter[window]", "all")
q.Add("sort", "-top")
case "popular":
default:
q.Add("filter[window]", "week")
q.Add("sort", "-viral")
sort = "popular"
}
req.URL.RawQuery = q.Encode()
res, err := http.DefaultClient.Do(req)
if err != nil {
return Tag{}, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return Tag{}, err
}
data := gjson.Parse(string(body))
wg := sync.WaitGroup{}
posts := make([]Submission, 0)
data.Get("posts").ForEach(
func(key, value gjson.Result) bool {
wg.Add(1)
go func() {
defer wg.Done()
posts = append(posts, Submission{
Id: value.Get("id").String(),
Title: value.Get("title").String(),
Link: strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""),
Cover: Media{
Id: value.Get("cover_id").String(),
Type: value.Get("cover.type").String(),
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
},
Points: value.Get("point_count").Int(),
Upvotes: value.Get("upvote_count").Int(),
Downvotes: value.Get("downvote_count").Int(),
Comments: value.Get("comment_count").Int(),
Views: value.Get("view_count").Int(),
IsAlbum: value.Get("is_album").Bool(),
})
}()
return true
},
)
wg.Wait()
tagData := Tag{
Tag: tag,
Display: data.Get("display").String(),
Sort: sort,
PostCount: data.Get("post_count").Int(),
Posts: posts,
Background: "/" + data.Get("background_id").String() + ".webp",
}
client.Cache.Set(tag + sort + page + "-tag", tagData, cache.DefaultExpiration)
return tagData, nil
}

View File

@@ -1,107 +0,0 @@
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
}

View File

@@ -1,86 +1,62 @@
package api package api
import ( import (
"encoding/json"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
"time" "time"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/video-prize-ranch/rimgo/types"
"github.com/patrickmn/go-cache" "github.com/spf13/viper"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
type User struct { func FetchUser(username string) (types.User, error) {
Id int64 res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID"))
Bio string
Username string
Points int64
Cover string
Avatar string
CreatedAt string
}
type Submission struct {
Id string
Title string
Link string
Cover Media
Points int64
Upvotes int64
Downvotes int64
Comments int64
Views int64
IsAlbum bool
}
func (client *Client) FetchUser(username string) (User, error) {
cacheData, found := client.Cache.Get(username + "-user")
if found {
return cacheData.(User), nil
}
res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + utils.Config.ImgurId)
if err != nil { if err != nil {
return User{}, err return types.User{}, err
} }
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return User{}, err return types.User{}, err
} }
data := gjson.Parse(string(body)) var user types.User
err = json.Unmarshal(body, &user)
createdTime, _ := time.Parse(time.RFC3339, data.Get("created_at").String()) if err != nil {
return types.User{}, err
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) 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 return user, nil
} }
func (client *Client) FetchSubmissions(username string, sort string, page string) ([]Submission, error) { func FetchSubmissions(username string, sort string, page string) ([]types.Submission, error) {
cacheData, found := client.Cache.Get(username + "-submissions-" + sort + page) res, err := http.Get("https://api.imgur.com/3/account/" + username + "/submissions/" + page + "/" + sort + "?album_previews=1&client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID"))
if 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 { if err != nil {
return []Submission{}, err return []types.Submission{}, err
} }
submissions := []Submission{} body, err := io.ReadAll(res.Body)
if err != nil {
return []types.Submission{}, err
}
data := gjson.Parse(string(body))
submissions := []types.Submission{}
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
data.Get("data").ForEach( data.Get("data").ForEach(
@@ -90,159 +66,35 @@ func (client *Client) FetchSubmissions(username string, sort string, page string
go func() { go func() {
defer wg.Done() defer wg.Done()
submissions = append(submissions, parseSubmission(value)) 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(),
})
}() }()
return true return true
}, },
) )
wg.Wait() wg.Wait()
client.Cache.Set(username+"-submissions-"+sort+page, submissions, 15*time.Minute)
return submissions, nil 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(),
}
}

4
config.yml Normal file
View File

@@ -0,0 +1,4 @@
RIMGU_PORT: 3000
RIMGU_HOST: localhost
RIMGU_ADDRESS: 0.0.0.0
RIMGU_IMGUR_CLIENT_ID: 546c25a59c58ad7

View File

@@ -2,16 +2,10 @@ version: '3'
services: services:
rimgo: rimgo:
image: codeberg.org/rimgo/rimgo # Official image #image: quay.io/pussthecatorg/rimgo # Uncomment to use image
#image: quay.io/pussthecatorg/rimgo # Unofficial image build: .
#build: . # Uncomment to build from source
ports: ports:
- 3000:3000 - 3000:3000
volumes:
- ./config.yml:/app/config.yml
restart: unless-stopped restart: unless-stopped
user: 65534:65534 # equivalent to `nobody`
read_only: true
security_opt:
- no-new-privileges
cap_drop:
- ALL
env_file: .env

57
go.mod
View File

@@ -1,41 +1,38 @@
module codeberg.org/rimgo/rimgo module codeberg.org/video-prize-ranch/rimgo
go 1.17 go 1.17
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 github.com/aws/aws-lambda-go v1.28.0
github.com/aymerick/raymond v2.0.2+incompatible github.com/awslabs/aws-lambda-go-api-proxy v0.12.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.0
github.com/gofiber/fiber/v2 v2.48.0 github.com/gofiber/fiber/v2 v2.24.0
github.com/gofiber/template/handlebars/v2 v2.1.4 github.com/gofiber/template v1.6.21
github.com/joho/godotenv v1.5.1 github.com/spf13/viper v1.10.1
github.com/microcosm-cc/bluemonday v1.0.25 github.com/tidwall/gjson v1.12.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/tidwall/gjson v1.14.4
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3
) )
require ( require (
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.2 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gofiber/template v1.8.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/gofiber/utils v1.1.0 // indirect github.com/klauspost/compress v1.13.4 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/magiconair/properties v1.8.5 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/klauspost/compress v1.16.7 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/spf13/afero v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.48.0 // indirect github.com/valyala/fasthttp v1.31.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.11.0 // indirect gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )

1062
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +0,0 @@
[
{
"url": "https://rimgo.pussthecat.org",
"countries": [
"de"
],
"cloudflare": false
},
{
"url": "https://rimgo.totaldarkness.net",
"countries": [
"ca"
],
"cloudflare": false
},
{
"url": "https://rimgo.bus-hit.me",
"countries": [
"ca"
],
"cloudflare": false
},
{
"url": "https://imgur.artemislena.eu",
"onion": "http://imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion",
"countries": [
"de"
],
"cloudflare": false
},
{
"url": "https://rimgo.vern.cc",
"onion": "http://rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion",
"countries": [
"ca"
],
"cloudflare": false
},
{
"url": "https://rim.odyssey346.dev",
"countries": [
"fr"
],
"cloudflare": false
},
{
"url": "https://imgur.010032.xyz",
"countries": [
"kr"
],
"cloudflare": false
},
{
"url": "https://i.habedieeh.re",
"onion": "http://i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion",
"i2p": [
"rimgo.i2p",
"rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p"
],
"countries": [
"ca"
],
"cloudflare": false
},
{
"url": "https://rimgo.hostux.net",
"countries": [
"fr"
],
"cloudflare": false
},
{
"url": "https://ri.zzls.xyz",
"onion": "http://rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion",
"i2p": [
"rimgo.zzls.i2p",
"p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p"
],
"countries": [
"cl"
],
"cloudflare": false
},
{
"url": "https://rimgo.lunar.icu",
"countries": [
"de"
],
"cloudflare": true
},
{
"url": "https://rimgo.kling.gg",
"onion": "http://tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion",
"i2p": "http://ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p",
"countries": [
"nl"
],
"cloudflare": false
},
{
"url": "https://i.01r.xyz",
"countries": [
"us"
],
"cloudflare": true
},
{
"url": "https://rimgo.eu.projectsegfau.lt",
"countries": [
"fr"
],
"cloudflare": false
},
{
"url": "https://rimgo.us.projectsegfau.lt",
"countries": [
"us"
],
"cloudflare": false
},
{
"url": "https://rimgo.in.projectsegfau.lt",
"countries": [
"in"
],
"cloudflare": false
},
{
"url": "https://rimgo.whateveritworks.org",
"countries": [
"de"
],
"cloudflare": true
},
{
"url": "https://rimgo.nohost.network",
"countries": [
"mx"
],
"cloudflare": false
},
{
"url": "https://rimgo.catsarch.com",
"countries": [
"us"
],
"cloudflare": false
},
{
"url": "https://rimgo.frontendfriendly.xyz",
"countries": [
"de"
],
"cloudflare": false
}
]

152
main.go
View File

@@ -1,139 +1,71 @@
package main package main
import ( import (
"flag" "context"
"fmt" "fmt"
"net/http" "net/http"
"os"
"time"
"codeberg.org/rimgo/rimgo/pages" "codeberg.org/video-prize-ranch/rimgo/pages"
"codeberg.org/rimgo/rimgo/static" "codeberg.org/video-prize-ranch/rimgo/static"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/video-prize-ranch/rimgo/views"
"codeberg.org/rimgo/rimgo/views" "github.com/aws/aws-lambda-go/events"
"github.com/aymerick/raymond" "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/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache" "github.com/gofiber/template/handlebars"
"github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/spf13/viper"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/template/handlebars/v2"
"github.com/joho/godotenv"
) )
func main() { var fiberLambda *fiberadaptor.FiberLambda
envPath := flag.String("c", ".env", "Path to env file")
godotenv.Load(*envPath)
utils.LoadConfig()
pages.InitializeApiClient() func init() {
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath("/etc/rimgu/")
viper.AddConfigPath("$HOME/.config/rimgu")
viper.AddConfigPath(".")
viper.AutomaticEnv()
views := http.FS(views.GetFiles()) viper.SetDefault("RIMGU_PORT", "3000")
if os.Getenv("ENV") == "dev" { viper.SetDefault("RIMGU_HOST", "localhost")
views = http.Dir("./views") viper.SetDefault("RIMGU_ADDRESS", "0.0.0.0")
viper.SetDefault("RIMGU_IMGUR_CLIENT_ID", "546c25a59c58ad7")
err := viper.ReadInConfig()
if err != nil {
fmt.Println(err)
} }
engine := handlebars.NewFileSystem(views, ".hbs")
engine.AddFunc("noteq", func(a interface{}, b interface{}, options *raymond.Options) interface{} {
if raymond.Str(a) != raymond.Str(b) {
return options.Fn()
}
return ""
})
engine := handlebars.NewFileSystem(http.FS(views.GetFiles()), ".hbs")
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Views: engine, Views: engine,
Prefork: utils.Config.FiberPrefork, Prefork: viper.GetBool("FIBER_PREFORK"),
UnescapePath: true, UnescapePath: true,
StreamRequestBody: true, StreamRequestBody: false,
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
err = ctx.Status(code).Render("errors/error", fiber.Map{
"err": err,
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return nil
},
}) })
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
fmt.Println(e)
},
}))
if os.Getenv("ENV") == "dev" {
app.Use("/static", filesystem.New(filesystem.Config{
Root: http.Dir("./static"),
}))
app.Get("/errors/429", func(c *fiber.Ctx) error {
return c.Render("errors/429", nil)
})
app.Get("/errors/404", func(c *fiber.Ctx) error {
return c.Render("errors/404", nil)
})
app.Get("/errors/error", func(c *fiber.Ctx) error {
return c.Render("errors/error", fiber.Map{
"err": "Test error",
})
})
} else {
app.Use("/static", filesystem.New(filesystem.Config{
MaxAge: 2592000,
Root: http.FS(static.GetFiles()),
}))
app.Use(cache.New(cache.Config{
Expiration: 30 * time.Minute,
MaxBytes: 25000000,
KeyGenerator: func(c *fiber.Ctx) string {
return c.OriginalURL()
},
CacheControl: true,
StoreResponseHeaders: true,
}))
}
app.Get("/robots.txt", func(c *fiber.Ctx) error { app.Get("/robots.txt", func(c *fiber.Ctx) error {
file, _ := static.GetFiles().ReadFile("robots.txt") file, _ := static.GetFiles().ReadFile("robots.txt")
_, err := c.Write(file) _, err := c.Write(file)
return err return err
}) })
app.Get("/favicon.ico", func(c *fiber.Ctx) error {
file, _ := static.GetFiles().ReadFile("favicon/favicon.ico")
_, err := c.Write(file)
return err
})
app.Get("/", pages.HandleFrontpage) app.Get("/", pages.FrontpageHandler)
app.Get("/about", pages.HandleAbout) app.Get("/:baseName.:extension", pages.HandleMedia)
app.Get("/privacy", pages.HandlePrivacy) app.Get("/a/:galleryID", pages.HandleGallery)
app.Get("/search", pages.HandleSearch) //app.Get("/t/:tagID", pages.HandleAlbum)
app.Get("/trending", pages.HandleTrending)
app.Get("/a/:postID", pages.HandlePost)
app.Get("/a/:postID/embed", pages.HandleEmbed)
app.Get("/t/:tag", pages.HandleTag)
app.Get("/t/:tag/:postID", pages.HandlePost)
app.Get("/r/:sub/:postID", pages.HandlePost)
app.Get("/user/:userID", pages.HandleUser) app.Get("/user/:userID", pages.HandleUser)
app.Get("/user/:userID/favorites", pages.HandleUserFavorites)
app.Get("/user/:userID/comments", pages.HandleUserComments)
app.Get("/user/:userID/cover", pages.HandleUserCover) app.Get("/user/:userID/cover", pages.HandleUserCover)
app.Get("/user/:userID/avatar", pages.HandleUserAvatar) app.Get("/user/:userID/avatar", pages.HandleUserAvatar)
app.Get("/gallery/:postID", pages.HandlePost) app.Get("/gallery/:galleryID", pages.HandleGallery)
app.Get("/gallery/:postID/embed", pages.HandleEmbed)
app.Get("/:postID.gifv", pages.HandleGifv)
app.Get("/:baseName.:extension", pages.HandleMedia)
app.Get("/stack/:baseName.:extension", pages.HandleMedia)
app.Get("/:postID", pages.HandlePost)
app.Get("/:postID/embed", pages.HandleEmbed)
app.Listen(utils.Config.Addr + ":" + utils.Config.Port) 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)
} }

View File

@@ -1,22 +0,0 @@
package pages
import (
"os"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleAbout(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return c.Render("about", fiber.Map{
"proto": c.Protocol(),
"domain": c.Hostname(),
"force_webp": os.Getenv("FORCE_WEBP"),
})
}

View File

@@ -1,12 +0,0 @@
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)
}

View File

@@ -1,48 +0,0 @@
package pages
import (
"strings"
"codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleEmbed(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
post, err := api.Album{}, error(nil)
switch {
case strings.HasPrefix(c.Path(), "/a"):
post, err = ApiClient.FetchAlbum(c.Params("postID"))
case strings.HasPrefix(c.Path(), "/gallery"):
post, err = ApiClient.FetchPosts(c.Params("postID"))
default:
post, err = ApiClient.FetchMedia(c.Params("postID"))
}
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", nil)
}
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return c.Status(404).Render("errors/404", nil)
}
if err != nil {
return err
}
return c.Render("embed", fiber.Map{
"post": post,
})
}
func HandleGifv(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
return c.Render("gifv", fiber.Map{
"id": c.Params("postID"),
})
}

View File

@@ -1,20 +1,14 @@
package pages package pages
import ( import (
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/video-prize-ranch/rimgo/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
var VersionInfo string func FrontpageHandler(c *fiber.Ctx) error {
func HandleFrontpage(c *fiber.Ctx) error {
utils.SetHeaders(c) utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=31557600") c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content") 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")
return c.Render("frontpage", fiber.Map{ return c.Render("frontpage", fiber.Map{})
"config": utils.Config,
"version": VersionInfo,
})
} }

34
pages/gallery.go Normal file
View File

@@ -0,0 +1,34 @@
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 Normal file
View File

@@ -0,0 +1,14 @@
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,
});
};

View File

@@ -1,21 +1,16 @@
package pages package pages
import ( import (
"io/ioutil"
"net/http" "net/http"
"os"
"strings"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/video-prize-ranch/rimgo/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func HandleMedia(c *fiber.Ctx) error { func HandleMedia(c *fiber.Ctx) error {
c.Set("Cache-Control", "public,max-age=31557600") c.Set("Cache-Control", "public,max-age=31557600")
if strings.HasPrefix(c.Path(), "/stack") { return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
return handleMedia(c, "https://i.stack.imgur.com/" + strings.ReplaceAll(c.Params("baseName"), "stack/", "") + "." + c.Params("extension"))
} else {
return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
}
} }
func HandleUserCover(c *fiber.Ctx) error { func HandleUserCover(c *fiber.Ctx) error {
@@ -30,50 +25,18 @@ func HandleUserAvatar(c *fiber.Ctx) error {
func handleMedia(c *fiber.Ctx, url string) error { func handleMedia(c *fiber.Ctx, url string) error {
utils.SetHeaders(c) 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")
if os.Getenv("FORCE_WEBP") == "1" && c.Query("no_webp") == "" && c.Accepts("image/webp") == "image/webp" && !strings.HasPrefix(c.Path(), "/stack") { res, err := http.Get(url)
url = strings.ReplaceAll(url, ".png", ".webp")
url = strings.ReplaceAll(url, ".jpg", ".webp")
url = strings.ReplaceAll(url, ".jpeg", ".webp")
}
if strings.HasPrefix(c.Path(), "/stack") && strings.Contains(c.OriginalURL(), "?") {
url = url + "?" + strings.Split(c.OriginalURL(), "?")[1]
}
req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return err return err
} }
utils.SetReqHeaders(req) body, err := ioutil.ReadAll(res.Body)
if c.Get("Range") != "" {
req.Header.Set("Range", c.Get("Range"))
}
res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
c.Status(res.StatusCode)
if res.StatusCode == 404 {
return c.Render("errors/404", fiber.Map{
"path": c.Path(),
})
} else if res.StatusCode == 429 {
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
c.Set("Accept-Ranges", "bytes")
c.Set("Content-Type", res.Header.Get("Content-Type")); c.Set("Content-Type", res.Header.Get("Content-Type"));
c.Set("Content-Length", res.Header.Get("Content-Length")) return c.Send(body)
if res.Header.Get("Content-Range") != "" {
c.Set("Content-Range", res.Header.Get("Content-Range"))
}
return c.SendStream(res.Body)
} }

View File

@@ -1,66 +0,0 @@
package pages
import (
"crypto/rand"
"fmt"
"strings"
"codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandlePost(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
post, err := api.Album{}, error(nil)
switch {
case strings.HasPrefix(c.Path(), "/a"):
post, err = ApiClient.FetchAlbum(c.Params("postID"))
case strings.HasPrefix(c.Path(), "/gallery"):
post, err = ApiClient.FetchPosts(c.Params("postID"))
case strings.HasPrefix(c.Path(), "/t"):
post, err = ApiClient.FetchPosts(c.Params("postID"))
default:
post, err = ApiClient.FetchMedia(c.Params("postID"))
}
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return c.Status(404).Render("errors/404", nil)
}
if err != nil {
return err
}
comments := []api.Comment{}
if post.SharedWithCommunity {
c.Set("Cache-Control", "public,max-age=604800")
comments, err = ApiClient.FetchComments(c.Params("postID"))
if err != nil {
return err
}
} else {
c.Set("Cache-Control", "public,max-age=31557600")
}
nonce := ""
csp := "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content; style-src 'self'"
if len(post.Tags) != 0 {
b := make([]byte, 8)
rand.Read(b)
nonce = fmt.Sprintf("%x", b)
csp = csp + " 'nonce-" + nonce + "'"
}
c.Set("Content-Security-Policy", csp)
return c.Render("post", fiber.Map{
"post": post,
"comments": comments,
"nonce": nonce,
})
}

View File

@@ -1,17 +0,0 @@
package pages
import (
"github.com/gofiber/fiber/v2"
"codeberg.org/rimgo/rimgo/utils"
)
func HandlePrivacy(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
return c.Render("privacy", fiber.Map{
"config": utils.Config,
"version": VersionInfo,
})
}

View File

@@ -1,50 +0,0 @@
package pages
import (
"strconv"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleSearch(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
query := c.Query("q")
if utils.ImgurRe.MatchString(query) {
return c.Redirect(utils.ImgurRe.ReplaceAllString(query, ""))
}
page := "0"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 0
}
displayPrevPage := true
if page == "0" {
displayPrevPage = false
}
results, err := ApiClient.Search(query, page)
if err != nil {
return err
}
return c.Render("search", fiber.Map{
"query": query,
"results": results,
"page": pageNumber + 1,
"displayPrev": displayPrevPage,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -1,51 +0,0 @@
package pages
import (
"strconv"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleTag(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := "1"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 0
}
displayPrevPage := true
if page == "1" {
displayPrevPage = false
}
tag, err := ApiClient.FetchTag(c.Params("tag"), c.Query("sort"), page)
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
if tag.Display == "" {
return c.Status(404).Render("errors/404", nil)
}
return c.Render("tag", fiber.Map{
"tag": tag,
"page": page,
"displayPrev": displayPrevPage,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -1,58 +0,0 @@
package pages
import (
"strconv"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleTrending(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := "1"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 1
}
section := c.Query("section")
switch section {
case "hot", "new", "top":
default:
section = "hot"
}
sort := c.Query("sort")
switch sort {
case "newest", "best", "popular":
default:
sort = "popular"
}
displayPrevPage := true
if page == "1" {
displayPrevPage = false
}
results, err := ApiClient.FetchTrending(section, sort, page)
if err != nil {
return err
}
return c.Render("trending", fiber.Map{
"results": results,
"section": section,
"sort": sort,
"page": pageNumber,
"displayPrev": displayPrevPage,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -1,142 +1,42 @@
package pages package pages
import ( import (
"strconv" "sync"
"codeberg.org/rimgo/rimgo/utils" "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" "github.com/gofiber/fiber/v2"
) )
func HandleUser(c *fiber.Ctx) error { func HandleUser(c *fiber.Ctx) error {
utils.SetHeaders(c) utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800") c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content") 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")
page := "0" wg := sync.WaitGroup{}
if c.Query("page") != "" { wg.Add(2)
page = c.Query("page") user, err := types.User{}, error(nil)
} go func() {
defer wg.Done()
pageNumber, err := strconv.Atoi(c.Query("page")) user, err = api.FetchUser(c.Params("userID"))
if err != nil { }()
pageNumber = 0
}
user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
if user.Username == "" {
return c.Status(404).Render("errors/404", nil)
}
submissions, err := ApiClient.FetchSubmissions(c.Params("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil { if err != nil {
return err 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 c.Render("user", fiber.Map{
"user": user, "user": user,
"submissions": submissions, "submissions": submissions,
"page": page,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}
func HandleUserComments(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
if user.Username == "" {
return c.Status(404).Render("errors/404", nil)
}
comments, err := ApiClient.FetchUserComments(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
return c.Render("userComments", fiber.Map{
"user": user,
"comments": comments,
})
}
func HandleUserFavorites(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := "0"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 0
}
user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
if user.Username == "" {
return c.Status(404).Render("errors/404", nil)
}
favorites, err := ApiClient.FetchUserFavorites(c.Params("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
return c.Render("userFavorites", fiber.Map{
"user": user,
"favorites": favorites,
"page": page,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
}) })
} }

11
src/config.ts Normal file
View File

@@ -0,0 +1,11 @@
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 Normal file
View File

@@ -0,0 +1,143 @@
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 Normal file
View File

@@ -0,0 +1,11 @@
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>'
);

View File

@@ -1,54 +0,0 @@
.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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 B

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/static/favicon/mstile-150x150.png"/>
<TileColor>#603cba</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

View File

@@ -1,19 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 917 B

View File

@@ -1,19 +0,0 @@
{
"name": "rimgo",
"short_name": "rimgo",
"icons": [
{
"src": "/static/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 347 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 334 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 239 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 376 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 355 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 536 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 711 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 860 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 549 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 265 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 555 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 313 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 673 B

View File

@@ -1,56 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,74 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0 24vw;
}
p a {
@apply break-words underline
}
.posts {
margin-top: 1em;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-auto-rows: 1fr;
gap: 1rem;
}
.posts img:not(.icon),
.posts video:not(:fullscreen) {
object-fit: cover;
aspect-ratio: 1;
}
#comments__expandBtn {
display: none;
}
.comments__expandBtn__label {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
text-decoration: none;
color: #fff
}
.comments__expandBtn__icon {
font-size: 24px;
}
#comments__expandBtn ~ .comments__expandBtn__label > span::after {
content: "⌄";
}
#comments__expandBtn:checked ~ .comments__expandBtn__label > span::after {
content: "⌃";
}
#comments__expandBtn:checked ~ .comments {
display: none;
}
.comment__media {
height: 12em;
display: block;
margin: 0.5em 0;
}
@media only screen and (max-width: 1280px) {
body {
margin: 0 8vw;
}
}
@media only screen and (max-width: 812px) {
body {
margin: 0 4vw;
}
}

View File

@@ -1,8 +0,0 @@
module.exports = {
content: ["./views/*.hbs", "./views/**/*.hbs"],
theme: {
extend: {},
},
variants: {},
plugins: [],
};

14
types/Album.go Normal file
View File

@@ -0,0 +1,14 @@
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
}

15
types/Comment.go Normal file
View File

@@ -0,0 +1,15 @@
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
}

11
types/Media.go Normal file
View File

@@ -0,0 +1,11 @@
package types
type Media struct {
Id string
Name string
Title string
Description string
Url string
Type string
MimeType string
}

24
types/User.go Normal file
View File

@@ -0,0 +1,24 @@
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
}

View File

@@ -1,65 +0,0 @@
package utils
import (
"os"
"time"
)
type config struct {
Port string
Addr string
ImgurId string
FiberPrefork bool
ImageCache bool
CleanupInterval time.Duration
CacheDir string
Privacy map[string]interface{}
}
var Config config
func LoadConfig() {
port := "3000"
if os.Getenv("PORT") != "" {
port = os.Getenv("PORT")
}
if os.Getenv("RIMGU_PORT") != "" {
port = os.Getenv("RIMGU_PORT")
}
addr := "0.0.0.0"
if os.Getenv("ADDRESS") != "" {
addr = os.Getenv("ADDRESS")
}
if os.Getenv("RIMGU_ADDRESS") != "" {
addr = os.Getenv("RIMGU_ADDRESS")
}
imgurId := "546c25a59c58ad7"
if os.Getenv("IMGUR_CLIENT_ID") != "" {
imgurId = os.Getenv("IMGUR_CLIENT_ID")
}
if os.Getenv("RIMGU_IMGUR_CLIENT_ID") != "" {
imgurId = os.Getenv("RIMGU_IMGUR_CLIENT_ID")
}
Config = config{
Port: port,
Addr: addr,
ImgurId: imgurId,
FiberPrefork: os.Getenv("FIBER_PREFORK") == "true",
Privacy: map[string]interface{}{
"set": os.Getenv("PRIVACY_NOT_COLLECTED") != "",
"policy": os.Getenv("PRIVACY_POLICY"),
"message": os.Getenv("PRIVACY_MESSAGE"),
"country": os.Getenv("PRIVACY_COUNTRY"),
"provider": os.Getenv("PRIVACY_PROVIDER"),
"cloudflare": os.Getenv("PRIVACY_CLOUDFLARE") == "true",
"not_collected": os.Getenv("PRIVACY_NOT_COLLECTED") == "true",
"ip": os.Getenv("PRIVACY_IP") == "true",
"url": os.Getenv("PRIVACY_URL") == "true",
"device": os.Getenv("PRIVACY_DEVICE") == "true",
"diagnostics": os.Getenv("PRIVACY_DIAGNOSTICS") == "true",
},
}
}

View File

@@ -1,5 +0,0 @@
package utils
import "regexp"
var ImgurRe = regexp.MustCompile(`https?://(i\.)?imgur\.com`)

View File

@@ -1,46 +0,0 @@
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))
}
}

View File

@@ -1,28 +1,12 @@
package utils package utils
import ( import "github.com/gofiber/fiber/v2"
"net/http"
"github.com/gofiber/fiber/v2"
)
func SetHeaders(c *fiber.Ctx) { func SetHeaders(c *fiber.Ctx) {
c.Set("X-Frame-Options", "DENY")
c.Set("Referrer-Policy", "no-referrer") c.Set("Referrer-Policy", "no-referrer")
c.Set("X-Content-Type-Options", "nosniff") c.Set("X-Content-Type-Options", "nosniff")
c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow") c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
c.Set("Strict-Transport-Security", "max-age=31557600") 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=(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=()") 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 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")
} }

View File

@@ -1,110 +0,0 @@
<!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>

View File

@@ -1,48 +0,0 @@
<!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>

View File

@@ -1,21 +0,0 @@
<!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>

View File

@@ -1,21 +0,0 @@
<!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>

View File

@@ -1,22 +0,0 @@
<!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>

View File

@@ -5,49 +5,54 @@
<title>rimgo</title> <title>rimgo</title>
{{> partials/head }} {{> partials/head }}
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/css/frontpage.css" />
</head> </head>
<body class="font-sans text-lg bg-slate-800 text-white"> <body>
{{> partials/nav }} {{> partials/header }}
<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"> <main>
<h2 class="font-bold text-white text-2xl">The fast, private image viewer for Imgur.</h2> <p>An alternative frontend for Imgur. Based on <a href="https://codeberg.org/3np/rimgu">rimgu</a> and rewritten in Go.</p>
{{> 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> <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>
</header>
<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>
<main class="my-8">
<h2 class="font-bold text-2xl">What is rimgo?</h2>
<p>
rimgo is an alternative frontend for Imgur.
Originally based on <a href="https://codeberg.org/3np/rimgu" rel="noreferrer">rimgu</a>.
It's a way to access Imgur without the ads, tracking, and other garbage.
rimgo is not affiliated with Imgur, all content is proxied from Imgur.
</p>
<br/>
<h3 class="font-bold text-xl">Notice</h3>
<p>All images and media are from Imgur. rimgo does not allow uploads or comments. Any issues with content should be reported to Imgur.</p>
</main> </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 }} {{> partials/footer }}
</body> </body>

93
views/gallery.hbs Normal file
View File

@@ -0,0 +1,93 @@
<!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>

View File

@@ -1,32 +0,0 @@
<!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>

View File

@@ -1,32 +1,25 @@
<div class="flex flex-col gap-2"> <div class="comment">
<div class="flex gap-2 items-center"> <div class="comment__user">
{{#noteq this.User.Username "[deleted]"}} <img src="{{this.User.Avatar}}" class="pfp" width="24" height="24" loading="lazy">
<img src="{{this.User.Avatar}}" class="rounded-full" width="24" height="24" loading="lazy">
<a href="/user/{{this.User.Username}}"> <a href="/user/{{this.User.Username}}">
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.User.Username}}</b></p> <p class="comment__user__username"><b>{{this.User.Username}}</b></p>
</a> </a>
{{/noteq}}
{{#equal this.User.Username "[deleted]"}}
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
{{/equal}}
</div> </div>
<div> <div>
<p>{{{this.Comment}}}</p> {{{this.Comment}}}
<div class="flex gap-2"> <p>
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span> <span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#if this.DeletedAt}} {{#if this.DeletedAt}}
<span class="text-md">(deleted {{this.DeletedAt}})</span> <span class="comment__updatedDate">(deleted {{this.DeletedAt}})</span>
{{/if}} {{/if}}
| |
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> {{this.Upvotes}} <span class="material-icons-outlined">thumb_up</span> {{this.Upvotes}}
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px"> {{this.Downvotes}} <span class="material-icons-outlined">thumb_down</span> {{this.Downvotes}}
</div> </p>
</div> </div>
{{#if this.Comments}} <div class="replies">
<div class="ml-4 p-2 border-solid border-l-2 border-slate-400">
{{#each this.Comments}} {{#each this.Comments}}
{{> partials/comment }} {{> partials/comment }}
{{/each}} {{/each}}
</div> </div>
{{/if}}
</div> </div>

View File

@@ -1,22 +0,0 @@
<a href="{{Post.Link}}">
<div class="sm:grid gap-4 w-full" style="grid-template-columns: 120px 1fr;">
<img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt="">
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
<div class="flex flex-col h-full">
<p class="md-container">{{{this.Comment}}}</p>
<div class="flex-grow"></div>
<div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#if this.DeletedAt}}
<span class="text-md">(deleted {{this.DeletedAt}})</span>
{{/if}}
|
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
{{this.Upvotes}}
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
{{this.Downvotes}}
</div>
</div>
</div>
</div>
</a>

View File

@@ -1,14 +1,10 @@
<footer class="mt-24 mb-12"> <br><br><br>
<div class="mb-1 flex gap-4 font-bold">
<a href="/"> <footer>
<div class="flex"> <!--<a href="/privacy">
<img src="/static/img/rimgo.svg" class="invert hue-rotate-180" width="32" height="32" /> <span class="material-icons-outlined">visibility</span> Privacy
<h1 class="text-md font-extrabold">rimgo</h1> </a>-->
</div> <a href="https://codeberg.org/video-prize-ranch/rimgo" rel="noreferrer">
</a> <span class="material-icons-outlined">code</span> Source Code
<a href="https://codeberg.org/rimgo/rimgo">Source Code</a> </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> </footer>

View File

@@ -2,14 +2,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/favicon-16x16.png">
<link rel="manifest" href="/static/favicon/site.webmanifest"> <link rel="manifest" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/site.webmanifest">
<link rel="mask-icon" href="/static/favicon/safari-pinned-tab.svg" color="#1e88e5"> <link rel="mask-icon" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/safari-pinned-tab.svg" color="#1e88e5">
<link rel="shortcut icon" href="/static/favicon/favicon.ico"> <link rel="shortcut icon" href="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/favicon.ico">
<meta name="msapplication-TileColor" content="#603cba"> <meta name="msapplication-TileColor" content="#603cba">
<meta name="msapplication-config" content="/static/favicon/browserconfig.xml"> <meta name="msapplication-config" content="https://d12bxz4wjlvqjb.cloudfront.net/static/favicon/browserconfig.xml">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="/static/app.css" /> <link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/fonts/Material-Icons-Outlined.css" />
<link rel="stylesheet" href="https://d12bxz4wjlvqjb.cloudfront.net/static/css/base.css"/>

View File

@@ -0,0 +1,8 @@
<nav>
<a href="/">
<img src="https://d12bxz4wjlvqjb.cloudfront.net/static/img/rimgo.svg" width="64" height="64" class="logo">
</a>
<a href="/">
<h2>rimgo</h2>
</a>
</nav>

View File

@@ -1,6 +0,0 @@
<a href="/">
<nav class="m-4 flex items-center justify-center">
<img src="/static/img/rimgo.svg" class="invert hue-rotate-180" width="72" height="72" />
<h1 class="font-extrabold text-3xl">rimgo</h1>
</nav>
</a>

View File

@@ -1,27 +1,32 @@
<a href="{{Link}}"> <a href="{{Link}}">
<div class="bg-slate-600 rounded-lg"> <div class="post">
{{#equal Cover.Type "video"}} {{#equal Cover.Type "video"}}
<video controls loop poster="/{{Cover.Id}}.webp" preload="none" width="100%" height="100%"> <video fullscreen controls loop poster="/{{Cover.Id}}.webp" preload="none" width="100%" height="100%">
<source src="{{Cover.Url}}" type="video/mp4" /> <source src="{{Cover.Url}}" type="video/mp4" />
</video> </video>
{{/equal}} {{/equal}}
{{#equal Cover.Type "image"}} {{#equal Cover.Type "image"}}
<img src="{{Cover.Url}}" loading="lazy" width="100%" height="100%"> <img src="{{Cover.Url}}" loading="lazy" width="100%" height="100%">
{{/equal}} {{/equal}}
<p class="m-2 text-ellipsis whitespace-nowrap overflow-hidden">{{Title}}</p> <p class="post__title">{{Title}}</p>
<div class="flex gap-2 p-2"> <div class="post__meta">
<div class="flex gap-1"> <p>
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Points" width="18px" height="18px"> <span class="material-icons-outlined">
arrow_upward
</span>
{{Points}} {{Points}}
</div> </p>
<div class="flex gap-1"> <p>
<img class="invert icon" src="/static/icons/PhChat.svg" alt="Comments" width="18px" height="18px"> <span class="material-icons-outlined">
comment
</span>
{{Comments}} {{Comments}}
</div> <p>
<div class="flex gap-1"> <span class="material-icons-outlined">
<img class="invert icon" src="/static/icons/PhEye.svg" alt="Views" width="18px" height="18px"> visibility
</span>
{{Views}} {{Views}}
</div> </p>
</div> </div>
</div> </div>
</a> </a>

View File

@@ -1,48 +0,0 @@
{{#if config.Privacy.not_collected}}
<div class="flex flex-col items-center w-full lg:w-1/2 p-4 bg-slate-600 rounded-lg">
<img class="invert" src="/static/icons/PhCheckCircle.svg" alt="" height="36" width="36" />
<h2 class="font-bold text-xl">Data not collected</h2>
<p class="text-lg">This instance does not collect any data.</p>
</div>
{{/if}}
{{#unless config.Privacy.set}}
<div class="flex flex-col items-center w-full lg:w-1/2 p-4 bg-slate-600 rounded-lg">
<img class="invert" src="/static/icons/PhWarning.svg" alt="" height="36" width="36" />
<h2 class="font-bold text-xl">No details provided</h2>
<p class="text-lg">The operator of this instance will be required to provide privacy details to be added to the instance list.</p>
</div>
{{else}}
{{#unless config.Privacy.not_collected}}
<div class="flex flex-col items-center w-full lg:w-1/2 p-4 bg-slate-600 rounded-lg">
<img class="invert" src="/static/icons/PhWarningCircle.svg" alt="" height="36" width="36" />
<h2 class="font-bold text-xl">Data collected</h2>
<p class="text-lg">The following data may be collected:</p>
<ul class="flex flex-col">
{{#if config.Privacy.ip}}
<li class="flex gap-1">
<img class="invert" src="/static/icons/PhGlobe.svg" alt="" width="24px" height="24px" />
Internet address (IP Address)
</li>
{{/if}}
{{#if config.Privacy.url}}
<li class="flex gap-1">
<img class="invert" src="/static/icons/PhLink.svg" alt="" width="24px" height="24px" />
Page viewed (Request URL)
</li>
{{/if}}
{{#if config.Privacy.device}}
<li class="flex gap-1">
<img class="invert" src="/static/icons/PhDevices.svg" alt="" width="24px" height="24px" />
Device Type (User agent)
</li>
{{/if}}
{{#if config.Privacy.diagnostics}}
<li class="flex gap-1">
<img class="invert" src="/static/icons/PhWrench.svg" alt="" width="24px" height="24px" />
Diagnostics
</li>
{{/if}}
</ul>
</div>
{{/unless}}
{{/unless}}

View File

@@ -1,26 +0,0 @@
<div class="bg-slate-600 rounded-lg">
<a href="{{Url}}">
<img src="{{ImageUrl}}?no_webp=1" loading="lazy" width="100%" height="100%">
</a>
<p class="m-2 text-ellipsis whitespace-nowrap overflow-hidden">
<a href="{{Url}}" title="{{Title}}">{{Title}}</a><br />
{{#if User}}
by <a href="/user/{{User}}" title="{{User}}">{{User}}</a>
{{else}}
<br />
{{/if}}
</p>
<div class="flex gap-2 p-2">
<p title="{{RelTime}}" class="text-ellipsis whitespace-nowrap overflow-hidden">
{{RelTime}}
</p>
<div class="flex gap-1">
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Points" width="18px" height="18px">
{{Points}}
</div>
<div class="flex gap-1">
<img class="invert icon" src="/static/icons/PhEye.svg" alt="Views" width="18px" height="18px">
{{Views}}
</div>
</div>
</div>

View File

@@ -1,6 +0,0 @@
<form method="GET" action="/search" class="flex gap-2 w-full lg:w-1/2">
<input class="p-2 rounded-lg bg-slate-600 w-full" name="q" type="text" placeholder="Search or paste a link" {{#if query}}value="{{query}}"{{/if}}/>
<button type="submit" class="p-2 rounded-lg bg-slate-600">
<img class="invert icon" src="/static/icons/PhMagnifyingGlass.svg" alt="Search" width="24px" height="24px">
</button>
</form>

View File

@@ -1,120 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>
{{#if post.Title}}
{{post.Title}} -
{{/if}}
rimgo
</title>
{{> partials/head }}
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
{{> partials/nav }}
<section class="my-4 w-full flex flex-col items-center">
{{> partials/searchBar }}
</section>
<header>
<h1 class="text-3xl font-bold">{{post.Title}}</h1>
<p>{{post.CreatedAt}}</p>
</header>
<main>
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center my-4">
{{#if post.User.Username}}
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
<p>
<b>{{post.User.Username}}</b>
</p>
</a>
{{/if}}
<div class="flex gap-2 items-center">
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
<p>{{post.Views}}</p>
</div>
{{#if post.SharedWithCommunity}}
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
<p>{{post.Upvotes}}</p>
</div>
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
<p>{{post.Downvotes}}</p>
</div>
{{/if}}
</div>
</div>
<div class="flex flex-center flex-col">
{{#each post.Media}}
{{#if this.Title}}
<h4 class="font-bold">{{this.Title}}</h4>
{{/if}}
{{#equal this.Type "image"}}
<img class="my-2 max-h-96 object-contain" src="{{this.Url}}" loading="lazy">
{{/equal}}
{{#equal this.Type "video"}}
<video class="my-2 max-h-96 object-contain" controls loop>
<source type="{{this.MimeType}}" src="{{this.Url}}" />
</video>
{{/equal}}
{{#if this.Description}}
<p>{{{this.Description}}}</p>
{{/if}}
{{/each}}
</div>
{{#if post.tags}}
<div class="flex gap-2 my-2 flex-wrap">
<style nonce="{{nonce}}">
{{#each post.tags}}
.{{this.BackgroundId}} { background-image: url('{{this.Background}}') }
{{/each}}
</style>
{{#each post.tags}}
<a href="/t/{{this.Tag}}">
<div class="rounded-md p-4 min-w-[110px] bg-slate-500 {{this.BackgroundId}}">
<p class="font-bold text-white text-center">
{{#if tag.Display}}
{{this.Display}}
{{else}}
{{this.Tag}}
{{/if}}
</p>
</div>
</a>
{{/each}}
</div>
{{/if}}
</main>
<section>
{{#if comments}}
<div>
<input id="comments__expandBtn" type="checkbox" checked>
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" for="comments__expandBtn">
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
<span class="text-xl font-bold"></span>
</label>
<div class="comments flex flex-col gap-2">
{{#each comments}}
{{> partials/comment }}
{{/each}}
</div>
</div>
{{/if}}
</section>
{{> partials/footer }}
</body>
</html>

View File

@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Privacy - rimgo</title>
{{> partials/head }}
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
{{> partials/nav }}
<section class="my-4 w-full flex flex-col items-center">
{{> partials/searchBar }}
</section>
<header>
<h1 class="text-3xl font-bold">Instance Privacy</h1>
{{#if config.Privacy.policy}}
<p>The instance operator has indicated their instance's privacy practices below. For more information, see the
instance operator's <a href="{{config.Privacy.policy}}">privacy policy</a>.</p>
{{else}}
<p>The instance operator has indicated their instance's privacy practices below.</p>
{{/if}}
{{#if config.Privacy.message}}
<p class="my-4">{{{config.Privacy.message}}}</p>
{{/if}}
</header>
<main class="flex justify-center w-full">
{{> partials/privacy }}
</main>
<section>
<h2 class="text-2xl my-4 font-bold">What is instance privacy?</h2>
<p>Instance privacy aims to bring transparency to the data collected by frontends and encourage privacy friendly practices. There is often no privacy policy and users are forced to trust that the instance operator is not collecting data. However, there is a possibility that the instance operator can put false information so we encourage looking at other factors when selecting an instance.</p>
<ul class="flex flex-col list-outside list-disc mt-2 ml-4">
<li>
<img class="inline invert" src="/static/icons/PhGlobe.svg" alt="" width="24px" height="24px" />
Internet address (IP Address) - This is an address that is given to your computer or internet connection.
This can be used to find your city or region and internet provider but can change depending on your connection.
</li>
<li>
<img class="inline invert" src="/static/icons/PhLink.svg" alt="" width="24px" height="24px" />
Page viewed (Request URL) - This is what page you are viewing.
</li>
<li>
<img class="inline invert" src="/static/icons/PhDevices.svg" alt="" width="24px" height="24px" />
Device Type (User agent) - This is what browser, device, and operating system you are using.
Advanced users can change this with an extension or browser setting.
</li>
<li>
<img class="inline invert" src="/static/icons/PhWrench.svg" alt="" width="24px" height="24px" />
Diagnostics - When this data is only collected when there is an error or only a short amount of time while
diagnosing an issue.
</li>
</ul>
</section>
<section>
<h2 class="text-2xl my-4 font-bold">Additional information</h2>
<ul>
<li>Version: {{version}}</li>
<li>Country: {{config.Privacy.country}}</li>
<li>Provider: {{config.Privacy.provider}}</li>
{{#if config.Privacy.cloudflare}}
<li>Using Cloudflare?: Yes</li>
{{else}}
<li>Using Cloudflare?: No</li>
{{/if}}
</ul>
</section>
{{> 'partials/footer' }}
</body>
</html>

View File

@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{query}} - rimgo</title>
{{> partials/head }}
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
{{> partials/nav }}
<section class="my-4 w-full flex flex-col items-center">
{{> partials/searchBar }}
</section>
<main>
<div class="posts">
{{#each results}}
{{> partials/result }}
{{/each}}
</div>
<div class="flex justify-between mt-4 font-bold">
{{#if displayPrev}}
<a href="/search?q={{query}}&page={{prevPage}}">Previous page</a>
{{/if}}
<p>Page {{page}}</p>
<a href="/search?q={{query}}&page={{nextPage}}">Next page</a>
</div>
</main>
{{> partials/footer }}
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{tag.Display}} - rimgo</title>
{{> partials/head }}
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
{{> partials/nav }}
<section class="my-4 w-full flex flex-col items-center">
{{> partials/searchBar }}
</section>
<header class="p-4 rounded-xl text-white mb-4" style="background-image: url('{{tag.Background}}');">
<div class="flex flex-col items-center justify-center text-center">
<h2 class="text-2xl font-bold">{{tag.Display}}</h2>
<p>{{tag.PostCount}} posts</p>
</div>
<div class="flex flex-col">
{{#equal tag.Sort "popular"}}
<a href="?sort=popular"><b>Popular</b></a>
<a href="?sort=newest">Newest</a>
<a href="?sort=best">Best</a>
{{/equal}}
{{#equal tag.Sort "newest"}}
<a href="?sort=popular">Popular</a>
<a href="?sort=newest"><b>Newest</b></a>
<a href="?sort=best">Best</a>
{{/equal}}
{{#equal tag.Sort "best"}}
<a href="?sort=popular">Popular</a>
<a href="?sort=newest">Newest</a>
<a href="?sort=best"><b>Best</b></a>
{{/equal}}
</div>
</header>
<main>
<div class="posts">
{{#each tag.Posts}}
{{> partials/post }}
{{/each}}
</div>
<div class="mt-4 font-bold">
{{#if displayPrev}}
<a href="{{channel.RelUrl}}?page={{prevPage}}">Previous page</a>
{{/if}}
<a href="{{channel.RelUrl}}?page={{nextPage}}">Next page</a>
</div>
</main>
{{> partials/footer }}
</body>
</html>

View File

@@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Trending - rimgo</title>
{{> partials/head }}
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
{{> partials/nav }}
<section class="my-4 w-full flex flex-col items-center">
{{> partials/searchBar }}
</section>
<header class="p-4 rounded-xl text-white mb-4 bg-gradient-to-r from-blue-400 to-emerald-400">
<div class="flex flex-col items-center justify-center text-center">
<h2 class="text-2xl font-extrabold">Trending</h2>
</div>
<div class="flex flex-col sm:flex-row sm:justify-between">
<div class="flex flex-col">
{{#equal section "hot"}}
<a href="?section=hot&sort={{sort}}"><b>Hot</b></a>
<a href="?section=new&sort={{sort}}">New</a>
<a href="?section=top&sort={{sort}}">Top</a>
{{/equal}}
{{#equal section "new"}}
<a href="?section=hot&sort={{sort}}">Hot</a>
<a href="?section=new&sort={{sort}}"><b>New</b></a>
<a href="?section=top&sort={{sort}}">Top</a>
{{/equal}}
{{#equal section "top"}}
<a href="?section=hot&sort={{sort}}">Hot</a>
<a href="?section=new&sort={{sort}}">New</a>
<a href="?section=top&sort={{sort}}"><b>Top</b></a>
{{/equal}}
</div>
<hr class="sm:hidden my-2" />
<div class="flex flex-col sm:items-end">
{{#equal sort "popular"}}
<a href="?section={{section}}&sort=popular"><b>Popular</b></a>
<a href="?section={{section}}&sort=newest">Newest</a>
<a href="?section={{section}}&sort=best">Best</a>
{{/equal}}
{{#equal sort "newest"}}
<a href="?section={{section}}&sort=popular">Popular</a>
<a href="?section={{section}}&sort=newest"><b>Newest</b></a>
<a href="?section={{section}}&sort=best">Best</a>
{{/equal}}
{{#equal sort "best"}}
<a href="?section={{section}}&sort=popular">Popular</a>
<a href="?section={{section}}&sort=newest">Newest</a>
<a href="?section={{section}}&sort=best"><b>Best</b></a>
{{/equal}}
</div>
</div>
</header>
<main>
<div class="posts">
{{#each results}}
{{> partials/post }}
{{/each}}
</div>
<div class="flex justify-between mt-4 font-bold">
{{#if displayPrev}}
<a href="/trending?section={{section}}&sort={{sort}}&page={{prevPage}}">Previous page</a>
{{/if}}
<p>Page {{page}}</p>
<a href="/trending?section={{section}}&sort={{sort}}&page={{nextPage}}">Next page</a>
</div>
</main>
{{> partials/footer }}
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More