diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 583decfd..516a569a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,12 @@ version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + # Maintain dependencies for npm + - package-ecosystem: "npm" directory: "/" schedule: interval: "daily" \ No newline at end of file diff --git a/.github/workflows/update-base-image.yml b/.github/workflows/update-base-image.yml index 99ce7796..ad700af9 100644 --- a/.github/workflows/update-base-image.yml +++ b/.github/workflows/update-base-image.yml @@ -10,26 +10,26 @@ jobs: steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: "Checkout repository" - uses: "actions/checkout@v2" + uses: "actions/checkout@v3" - name: Build and push to Docker Hub and Github Packages Docker Registry id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: push: true tags: | diff --git a/.github/workflows/update-image-on-push.yml b/.github/workflows/update-image-on-push.yml index d00eeff6..ac200885 100644 --- a/.github/workflows/update-image-on-push.yml +++ b/.github/workflows/update-image-on-push.yml @@ -9,26 +9,26 @@ jobs: steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: "Checkout repository" - uses: "actions/checkout@v2" + uses: "actions/checkout@v3" - name: Build and push to Docker Hub and Github Packages Docker Registry id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: push: true tags: | diff --git a/.gitignore b/.gitignore index 769b9396..7dd6f8ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ *.log config.js +.vscode/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..92cde390 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2710e7..f17382f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## Changelog has not been updated since 2021-03-23 + # Changelog ## 2021-03-23 ### Fixed diff --git a/Dockerfile b/Dockerfile index 028b14b2..773d1bd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -# Use LTS Node.js base image -FROM node:14.16-alpine +# Use LTS Node.js slim image +FROM node:slim # Video support dependency -RUN apk add ffmpeg +RUN apt-get update && apt-get install -y ffmpeg wget # Install NPM dependencies and copy the project WORKDIR /teddit @@ -12,4 +12,6 @@ COPY config.js.template ./config.js RUN find ./static/ -type d -exec chmod -R 777 {} \; +EXPOSE 8080 + CMD npm start diff --git a/README.md b/README.md index 48b51069..e830cff7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # teddit +## teddit is no more actively maintained! + +[Due to Reddit's API changes](https://en.wikipedia.org/wiki/2023_Reddit_API_controversy), this project is no more actively maintained. +Feel free to fork the project if you like, or contribute to other alternative Reddit front-ends, such as Libreddit, which are trying to come up with circumventions. + +This project is still maintained, but just not actively. You can create PRs, but don't expect them to be merged right away. + +--- + [teddit.net](https://teddit.net) A free and open source alternative Reddit front-end focused on privacy. @@ -22,25 +31,96 @@ XMR: 832ogRwuoSs2JGYg7wJTqshidK7dErgNdfpenQ9dzMghNXQTJRby1xGbqC3gW3GAifRM9E84J91 Community instances: -* [https://teddit.ggc-project.de](https://teddit.ggc-project.de) -* [https://teddit.kavin.rocks](https://teddit.kavin.rocks) -* [https://teddit.zaggy.nl](https://teddit.zaggy.nl) -* [https://teddit.namazso.eu](https://teddit.namazso.eu) -* [https://teddit.nautolan.racing](https://teddit.nautolan.racing) -* [https://teddit.tinfoil-hat.net](https://teddit.tinfoil-hat.net) -* [https://teddit.domain.glass](https://teddit.domain.glass) -* [ibarajztopxnuhabfu7f...onion](http://ibarajztopxnuhabfu7fg6gbudynxofbnmvis3ltj6lfx47b6fhrd5qd.onion) -* [xugoqcf2pftm76vbznx4...i2p](http://xugoqcf2pftm76vbznx4xuhrzyb5b6zwpizpnw2hysexjdn5l2tq.b32.i2p) - +| Instance | Onion Link | I2P | Notes | +|-|-|-|-| +| [teddit.ggc-project.de](https://teddit.ggc-project.de) | | | | +| [teddit.zaggy.nl](https://teddit.zaggy.nl) | | | | +| [teddit.tinfoil-hat.net](https://teddit.tinfoil-hat.net) | | | | +| [teddit.domain.glass](https://teddit.domain.glass) | | | | +| [snoo.ioens.is](https://snoo.ioens.is) | [snoo.ioensistjs7wd746...onion](http://snoo.ioensistjs7wd746zluwixvojbbkxhr37lepdvwtdfeav673o64iflqd.onion/) | | | +| [teddit.httpjames.space](https://teddit.httpjames.space) | | | | +| [teddit.xbdm.fun](https://teddit.xbdm.fun) | | | | +| | [ibarajztopxnuhabfu7f...onion](http://ibarajztopxnuhabfu7fg6gbudynxofbnmvis3ltj6lfx47b6fhrd5qd.onion) | [xugoqcf2pftm76vbznx4...i2p](http://xugoqcf2pftm76vbznx4xuhrzyb5b6zwpizpnw2hysexjdn5l2tq.b32.i2p) | Operated by [mdleom.com](https://mdleom.com/about/#Services) | +| [incogsnoo.com](https://incogsnoo.com) | [tedditfyn6idalzso5wam....onion](http://tedditfyn6idalzso5wam5qd3kdtxoljjhbrbbx34q2xkcisvshuytad.onion/) | [http://teddit.i2p](http://teddit.i2p) | | +| [teddit.pussthecat.org](https://teddit.pussthecat.org) | | | Operated by [PussTheCat.org](https://pussthecat.org/) | +| [reddit.lol](https://reddit.lol) | [http://dawtyi5e2cfyfmoht...onion](http://dawtyi5e2cfyfmoht4izmczi42aa2zwh6wi34zwvc6rzf2acpxhrcrad.onion) | [http://vzeiwzi7ogwl3i...b32.i2p](http://vzeiwzi7ogwl3ijrfek4fbtwhvamxcpyqoc3s4vcgnhlp54s5clq.b32.i2p) | Operated by https://liberta.casa | | +| [teddit.sethforprivacy.com](https://teddit.sethforprivacy.com/) | [qtpvyiaqhmwccx...onion/](http://qtpvyiaqhmwccxwzsqubd23xhmmrt75tdyw35kp43w4hvamsgl3x27ad.onion/) | | For more similar hosted tools, see [blog.sethforprivacy.com](https://blog.sethforprivacy.com/about/#my-community-resources) | +| [teddit.adminforge.de](https://teddit.adminforge.de) | | | Operated by https://adminforge.de | +| [teddit.bus-hit.me](https://teddit.bus-hit.me) | | | Operated by https://bus-hit.me | +| [teddit.froth.zone](https://teddit.froth.zone) | | | | +| [rdt.trom.tf](https://rdt.trom.tf) | | | Part of the https://trom.tf project | +| [teddit.encrypted-data.xyz](https://teddit.encrypted-data.xyz) | | | | +| [i.opnxng.com](https://i.opnxng.com) | | | | +| [teddit.tokhmi.xyz](https://teddit.tokhmi.xyz) | | | | +| [teddit.garudalinux.org](https://teddit.garudalinux.org) | | | Managed by https://garudalinux.org | +| [teddit.privacytools.io](https://teddit.privacytools.io) | [jnuonmf2n36sfdmyksqq....onion](http://jnuonmf2n36sfdmyksqqqyab3w63cq4kx24olyjleh5z6zzfvyt7uqqd.onion) | | Part of [PrivacyTools.io](https://www.privacytools.io/) and hosted by [Privex](https://www.privex.io/) | +| [td.vern.cc](https://td.vern.cc) | [td.vernccvbvyi5qhfzyqen...onion](http://td.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | [td.vern.i2p](http://verncco2oaxjikammz4pi7umzp673cme6zuemx7yeeewspwrw3va.b32.i2p) | Operated by https://vern.cc | +| [teddit.rawbit.ninja](https://teddit.rawbit.ninja) | [yqu4yj5lju7bmlwpzpml...onion](http://yqu4yj5lju7bmlwpzpmltb5gsu6cw7nnbcxxx4iqemwa56nxjiggf4qd.onion) | | Operated by https://rawbit.ninja | +| [teddit.artemislena.eu](https://teddit.artemislena.eu) | [teddit.lpoaj7z2zkajuhgnlltp...onion](http://teddit.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion) | | Operated by https://artemislena.eu | +| [teddit.hostux.net](https://teddit.hostux.net) | | | Operated by https://hostux.net | +| [teddit.no-logs.com](https://teddit.no-logs.com/) | | | Operated by https://no-logs.com | +| [teddit.projectsegfau.lt](https://teddit.projectsegfau.lt) | [teddit.pjsfkvpxlinjamta...onion](http://teddit.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion) | | Maintained by Project Segfault Team (https://projectsegfau.lt/team) | + + + ## Installation -### Docker-compose method +### Docker-compose method (production) + +```docker +version: "3.8" + +services: + + teddit: + container_name: teddit + image: teddit/teddit:latest + environment: + - DOMAIN=teddit.net + - USE_HELMET=true + - USE_HELMET_HSTS=true + - TRUST_PROXY=true + - REDIS_HOST=teddit-redis + ports: + - "127.0.0.1:8080:8080" + networks: + - teddit_net + healthcheck: + test: ["CMD", "wget" ,"--no-verbose", "--tries=1", "--spider", "http://localhost:8080/about"] + interval: 1m + timeout: 3s + depends_on: + - teddit-redis + + teddit-redis: + container_name: teddit-redis + image: redis:6.2.5-alpine + command: redis-server + environment: + - REDIS_REPLICATION_MODE=master + networks: + - teddit_net + +networks: + teddit_net: +``` + +Note: This compose is made for a true "production" setup, and is made to be used to have teddit behind a reverse proxy, if you don't want that and prefer to directly access teddit via its port: + +- Change `ports: - "127.0.0.1:8080:8080"` to `ports: - "8080:8080"` +- Remove `DOMAIN=teddit.net`, `USE_HELMET=true`, `USE_HELMET_HSTS=true`, `TRUST_PROXY=true` + + +### Docker-compose method (development) -```console -wget https://codeberg.org/teddit/teddit/raw/branch/main/docker-compose.yml +```bash +git clone https://codeberg.org/teddit/teddit +cd teddit docker-compose build docker-compose up ``` @@ -62,6 +142,7 @@ The following variables may be set to customize your deployment at runtime. | flairs_enabled | Enables the rendering of user and link flairs on Teddit. Defaults to **true** | | highlight_controversial | Enables controversial comments to be indicated by a typographical dagger (†). Defaults to **true** | | api_enabled | Teddit API feature. Might increase loads significantly on your instance. Defaults to **true** | +| api_force_https | Force HTTPS to Teddit API permalinks (see #285). Defaults to **false** | | video_enabled | Enables video playback within Teddit. Defaults to **true** | | redis_enabled | Enables Redis caching. If disabled, does not allow for any caching of Reddit API calls. Defaults to **true** | | redis_db | Sets the redis DB name, if required | @@ -80,9 +161,15 @@ The following variables may be set to customize your deployment at runtime. | use_helmet_hsts | *Boolean* Recommended to be true when using https. Defaults to **false** | | trust_proxy | *Boolean* Enable trust_proxy if you are using a reverse proxy like nginx or traefik. Defaults to **false** | | trust_proxy_address | Location of trust_proxy. Defaults to **127.0.0.1** | +| http_proxy | Set http/https proxy to use for outgoing requests. See [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) for details | | nsfw_enabled | *Boolean* Enable NSFW (over 18) content. If false, a warning is shown to the user before opening any NSFW post. When the NFSW content is disabled, NSFW posts are hidden from subreddits and from user page feeds. Note: Users can set this to true or false from their preferences. Defaults to **true** | +| videos_muted | *Boolean* Automatically mute all videos in posts. Defaults to **true** | | post_comments_sort | Defines default sort preference. Options are *confidence* (default sorting option in Reddit), *top*, *new*, *controversal*, *old*, *random*, *qa*, *live*. Defaults to **confidence** | | reddit_app_id | If "use_reddit_oauth" config key is set to true, you have to obtain your Reddit app ID. For testing purposes it's okay to use this project's default app ID. Create your Reddit app here: https://old.reddit.com/prefs/apps/. Make sure to create an "installed app" type of app. Default is **ABfYqdDc9qPh1w** | +| domain_replacements | Replacements for domains in outgoing links. Tuples with regular expressions to match, and replacement values. This is in addition to user-level configuration of privacyDomains. Defaults to **[]** | +| cache_control | *Boolean* If true, teddit will automatically remove all cached static files. Defaults to **true** | +| cache_control_interval | How often the cache directory for static files is emptied (in hours). Default is every 24 hours. Requires cache_control to be true. Defaults to **24** | +| suggested_subreddits | Array of suggested subreddits, which are displayed in the top bar (if the user doesn't have any subscriptions) and in the cleaned home page. Defaults to Reddit's default suggested subreddits. | ### Manual @@ -90,13 +177,13 @@ The following variables may be set to customize your deployment at runtime. 1. (Optional) Install [redis-server](https://redis.io). - Highly recommended – it works as a cache for Reddit API calls. + Highly recommended – it works as a cache for Reddit API calls. 1. (Optional) Install [ffmpeg](https://ffmpeg.org). It's needed if you want to support videos. - ```console + ```bash # Linux apt install redis-server ffmpeg @@ -106,7 +193,7 @@ The following variables may be set to customize your deployment at runtime. 1. Clone and set up the repository. - ```console + ```bash git clone https://codeberg.org/teddit/teddit cd teddit npm install --no-optional @@ -115,4 +202,26 @@ The following variables may be set to customize your deployment at runtime. npm start ``` -Teddit should now be running at . +Teddit should now be running at . + +You can also run teddit from a process manager like [pm2](https://www.npmjs.com/package/pm2): + +``` +## To run: +npm install pm2 -g +pm2 start app.js --name teddit + +## To run on startup: +pm2 startup +pm2 save ## if using systemd, see below. + +## To restart or stop +pm2 restart teddit +pm2 stop teddit +``` + +See also the [pm2 instructions for running a project on startup](https://pm2.keymetrics.io/docs/usage/startup/). In particular, if using systemd, see the section on how to modify the systemd init file so that it runs after your system connects to the network. + +## Legal + +Teddit does not host any content. All content shown on any Teddit instances is from Reddit™. Reddit is a trademark of Reddit Inc. Teddit is not affiliated with Reddit Inc. Any issues with content shown on any Teddit instances need to be reported to Reddit, not the instance host's internet provider or domain provider. diff --git a/app.js b/app.js index e718983d..bbc03f43 100644 --- a/app.js +++ b/app.js @@ -1,145 +1,141 @@ -const config = require('./config') - -global.client_id_b64 = Buffer.from(`${config.reddit_app_id}:`).toString('base64') -global.reddit_access_token = null -global.reddit_refresh_token = null -global.ratelimit_counts = {} -global.ratelimit_timestamps = {} - -const pug = require('pug') -const compression = require('compression') -const express = require('express') -const cookieParser = require('cookie-parser') -const r = require('redis') - -const redis = (() => { - if (!config.redis_enabled) { - // Stub Redis if disabled - return { - get: (_, callback) => callback(null, null), - setex: (_, _1, _2, callback) => callback(null), - on: () => {} - } - } - - const redisOptions = { - host: '127.0.0.1', - port: 6379 - } - - if (config.redis_db) { - redisOptions.db = config.redis_db - } - - if (config.redis_host) { - redisOptions.host = config.redis_host - } - - if (config.redis_port && config.redis_port > 0) { - redisOptions.port = config.redis_port - } - - if (config.redis_password) { - redisOptions.password = config.redis_password - } - - return r.createClient(redisOptions) -})() -const helmet = require('helmet') -const bodyParser = require('body-parser') -const fetch = require('node-fetch') -const fs = require('fs') -const app = express() -const request = require('postman-request') -const commons = require('./inc/commons.js')(request, fs) +const config = require('./config'); + +global.client_id_b64 = Buffer.from(`${config.reddit_app_id}:`).toString( + 'base64' +); +global.reddit_access_token = null; +global.reddit_refresh_token = null; +global.ratelimit_counts = {}; +global.ratelimit_timestamps = {}; + +const pug = require('pug'); +const compression = require('compression'); +const express = require('express'); +const cookieParser = require('cookie-parser'); +const { redis } = require('./inc/redis'); + +const nodeFetch = require('node-fetch'); +const fetch = config.http_proxy + ? (() => { + const agent = require('https-proxy-agent')(config.http_proxy); + return (url, options) => { + const instanceOptions = { + agent, + ...options, + }; + return nodeFetch(url, instanceOptions); + }; + })() + : nodeFetch; + +const helmet = require('helmet'); +const bodyParser = require('body-parser'); +const fs = require('fs'); +const app = express(); +const request = require('postman-request'); +const commons = require('./inc/commons.js')(request, fs); const dlAndSave = require('./inc/downloadAndSave.js')(commons); ['pics/thumbs', 'pics/flairs', 'pics/icons', 'vids'] - .map(d => `./static/${d}`) - .filter(d => !fs.existsSync(d)) - .forEach(d => fs.mkdirSync(d, { recursive: true })) + .map((d) => `./static/${d}`) + .filter((d) => !fs.existsSync(d)) + .forEach((d) => fs.mkdirSync(d, { recursive: true })); -if(!config.https_enabled && config.redirect_http_to_https) { - console.error(`Cannot redirect HTTP=>HTTPS while "https_enabled" is false.`) +if (!config.https_enabled && config.redirect_http_to_https) { + console.error(`Cannot redirect HTTP=>HTTPS while "https_enabled" is false.`); } -let https = null -if(config.https_enabled) { - const privateKey = fs.readFileSync(`${config.cert_dir}/privkey.pem`, 'utf8') - const certificate = fs.readFileSync(`${config.cert_dir}/cert.pem`, 'utf8') - const ca = fs.readFileSync(`${config.cert_dir}/chain.pem`, 'utf8') +let https = null; +if (config.https_enabled) { + const privateKey = fs.readFileSync(`${config.cert_dir}/privkey.pem`, 'utf8'); + const certificate = fs.readFileSync(`${config.cert_dir}/cert.pem`, 'utf8'); + const ca = fs.readFileSync(`${config.cert_dir}/fullchain.pem`, 'utf8'); const credentials = { - key: privateKey, - cert: certificate, - ca: ca - } - https = require('https').Server(credentials, app) - global.protocol = 'https://' + key: privateKey, + cert: certificate, + ca: ca, + }; + https = require('https').Server(credentials, app); + global.protocol = 'https://'; } else { - global.protocol = 'http://' + global.protocol = 'http://'; } -const http = require('http').Server(app) +const http = require('http').Server(app); -if(config.redirect_www) { +if (config.redirect_www) { app.use((req, res, next) => { - if(req.headers.host) { - if(req.headers.host.slice(0, 4) === 'www.') { - let newhost = req.headers.host.slice(4) - return res.redirect(301, `${req.protocol}://${newhost}${req.originalUrl}`) + if (req.headers.host) { + if (req.headers.host.slice(0, 4) === 'www.') { + let newhost = req.headers.host.slice(4); + return res.redirect( + 301, + `${req.protocol}://${newhost}${req.originalUrl}` + ); } } - next() - }) + next(); + }); } -if(config.use_helmet && config.https_enabled) { - app.use(helmet()) - if(config.use_helmet_hsts) { - app.use(helmet.hsts({ maxAge: 31536000, preload: true })) +if (config.use_helmet && config.https_enabled) { + app.use(helmet()); + if (config.use_helmet_hsts) { + app.use(helmet.hsts({ maxAge: 31536000, preload: true })); } } -if(config.use_compression) { - app.use(compression()) +if (config.use_compression) { + app.use(compression()); } -app.use(cookieParser()) +app.use(cookieParser()); -if(config.use_view_cache) { - app.set('view cache', true) +if (config.use_view_cache) { + app.set('view cache', true); } -if(config.trust_proxy) { - app.set('trust proxy', config.trust_proxy_address) +if (config.trust_proxy) { + app.set('trust proxy', config.trust_proxy_address); } -app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })) -app.use(bodyParser.json({ limit: '10mb' })) -app.use(express.static(`${__dirname}/static`)) +app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); +app.use(bodyParser.json({ limit: '10mb' })); +app.use(express.static(`${__dirname}/static`)); -app.set('views', './views') -app.set('view engine', 'pug') +app.set('views', './views'); +app.set('view engine', 'pug'); -if(config.redirect_http_to_https) { +if (config.redirect_http_to_https) { app.use((req, res, next) => { - if(req.secure) - next() - else - res.redirect(`https://${req.headers.host}${req.url}`) - }) + if (req.secure) next(); + else res.redirect(`https://${req.headers.host}${req.url}`); + }); } -const redditAPI = require('./inc/initRedditApi.js')(fetch) -require('./routes')(app, redis, fetch, redditAPI) +const redditAPI = require('./inc/initRedditApi.js')(fetch); -redis.on('error', (error) => { - if(error) { - console.error(`Redis error: ${error}`) - } -}) +/* +This is temporary. It's needed for the routes to work. +It can be removed once these functions are made more modular. +*/ +module.exports = { redis, fetch, RedditAPI: redditAPI }; + +const allRoutes = require('./routes/index'); + +app.use('/', allRoutes); + +// The old routes +//require('./routes')(app, redis, fetch, redditAPI); + +const cacheControl = require('./cacheControl.js'); +cacheControl.removeCacheFiles(); -if(config.https_enabled) { - https.listen(config.ssl_port, '::', () => console.log(`Teddit running on https://${config.domain}:${config.ssl_port}`)) +if (config.https_enabled) { + https.listen(config.ssl_port, config.listen_address, () => + console.log(`Teddit running on https://${config.domain}:${config.ssl_port}`) + ); } -http.listen(config.nonssl_port, '::', () => console.log(`Teddit running on http://${config.domain}:${config.nonssl_port}`)) +http.listen(config.nonssl_port, config.listen_address, () => + console.log(`Teddit running on http://${config.domain}:${config.nonssl_port}`) +); diff --git a/cacheControl.js b/cacheControl.js new file mode 100644 index 00000000..a4c3fc63 --- /dev/null +++ b/cacheControl.js @@ -0,0 +1,32 @@ +module.exports.removeCacheFiles = function() { + const config = require('./config'); + + async function deleteStatic() { + const fs = require('fs'); + const pics = './static/pics/'; + const vids = './static/vids/'; + + fs.rmdir(pics, { recursive: true, force: true }, () => { + fs.rmdir(vids, { recursive: true, force: true }, () => { + ['pics/thumbs', 'pics/flairs', 'pics/icons', 'vids'].map((d) => `./static/${d}`) + .filter((d) => !fs.existsSync(d)) + .forEach((d) => fs.mkdirSync(d, { recursive: true })); + + console.log('Cleared cached static media files. You can turn this off by setting the config.cache_control to false.'); + }); + }); + } + + if(config.cache_control) { + deleteStatic(); + + let hours = config.cache_control_interval; + if (hours < 1 || hours > 10000 || isNaN(hours)) { + hours = 24; + } + + setInterval(() => { + deleteStatic(); + }, 1000 * 60 * 60 * hours); + } +} diff --git a/config.js.template b/config.js.template index 139f23e4..bdfb17fd 100644 --- a/config.js.template +++ b/config.js.template @@ -3,33 +3,41 @@ const config = { use_reddit_oauth: process.env.USE_REDDIT_OAUTH === 'true' || false, // If false, teddit uses Reddit's public API. If true, you need to have your own Reddit app ID (enter the app ID to the "reddit_app_id" config key). cert_dir: process.env.CERT_DIR || '', // For example '/home/teddit/letsencrypt/live/teddit.net', if you are using https. No trailing slash. theme: process.env.THEME || 'auto', // One of: 'dark', 'sepia', 'auto', ''. Auto theme uses browser's theme detection (Dark or White theme). White theme is set by the empty the option (''). - flairs_enabled: process.env.FLAIRS_ENABLED !== 'true' || true, // Enables the rendering of user and link flairs on teddit - highlight_controversial: process.env.HIGHLIGHT_CONTROVERSIAL !== 'true' || true, // Enables controversial comments to be indicated by a typographical dagger (†) - api_enabled: process.env.API_ENABLED !== 'true' || true, // Teddit API feature. Might increase loads significantly on your instance. - video_enabled: process.env.VIDEO_ENABLED !== 'true' || true, - redis_enabled: process.env.REDIS_ENABLED !== 'true' || true, // If disabled, does not cache Reddit API calls + clean_homepage: !('CLEAN_HOMEPAGE' in process.env) || process.env.CLEAN_HOMEPAGE === 'true', // Allows the clean homepage to be used (similar to invidious), instead of the usual reddit-like frontpage + flairs_enabled: !('FLAIRS_ENABLED' in process.env) || process.env.FLAIRS_ENABLED === 'true', // Enables the rendering of user and link flairs on teddit + highlight_controversial: !('HIGHLIGHT_CONTROVERSIAL' in process.env) || process.env.HIGHLIGHT_CONTROVERSIAL === 'true', // Enables controversial comments to be indicated by a typographical dagger (†) + api_enabled: !('API_ENABLED' in process.env) || process.env.API_ENABLED === 'true', // Teddit API feature. Might increase loads significantly on your instance. + api_force_https: process.env.API_FORCE_HTTPS === 'true' || false, // Force HTTPS to Teddit API permalinks (see #285). + video_enabled: !('VIDEO_ENABLED' in process.env) || process.env.VIDEO_ENABLED === 'true', + redis_enabled: !('REDIS_ENABLED' in process.env) || process.env.REDIS_ENABLED === 'true', // If disabled, does not cache Reddit API calls redis_db: process.env.REDIS_DB, redis_host: process.env.REDIS_HOST || '127.0.0.1', redis_password: process.env.REDIS_PASSWORD, redis_port: process.env.REDIS_PORT || 6379, ssl_port: process.env.SSL_PORT || 8088, nonssl_port: process.env.NONSSL_PORT || 8080, - listen_address: process.env.LISTEN_ADDRESS || '0.0.0.0', + listen_address: process.env.LISTEN_ADDRESS || '0.0.0.0', // '0.0.0.0' will accept connections only from IPv4 addresses. If you want to also accept IPv6 addresses use '::'. https_enabled: process.env.HTTPS_ENABLED === 'true' || false, redirect_http_to_https: process.env.REDIRECT_HTTP_TO_HTTPS === 'true' || false, redirect_www: process.env.REDIRECT_WWW === 'true' || false, - use_compression: process.env.USE_COMPRESSION !== 'true' || true, + use_compression: !('USE_COMPRESSION' in process.env) || process.env.USE_COMPRESSION === 'true', use_view_cache: process.env.USE_VIEW_CACHE === 'true' || false, use_helmet: process.env.USE_HELMET === 'true' || false, // Recommended to be true when using https use_helmet_hsts: process.env.USE_HELMET_HSTS === 'true' || false, // Recommended to be true when using https trust_proxy: process.env.TRUST_PROXY === 'true' || false, // Enable trust_proxy if you are using reverse proxy like nginx trust_proxy_address: process.env.TRUST_PROXY_ADDRESS || '127.0.0.1', - nsfw_enabled: process.env.NSFW_ENABLED !== 'true' || true, // Enable NSFW (over 18) content. If false, a warning is shown to the user before opening any NSFW post. When the NFSW content is disabled, NSFW posts are hidden from subreddits and from user page feeds. Note: Users can set this to true or false from their preferences. + http_proxy: process.env.HTTP_PROXY, + nsfw_enabled: !('NSFW_ENABLED' in process.env) || process.env.NSFW_ENABLED === 'true', // Enable NSFW (over 18) content. If false, a warning is shown to the user before opening any NSFW post. When the NFSW content is disabled, NSFW posts are hidden from subreddits and from user page feeds. Note: Users can set this to true or false from their preferences. + videos_muted: !('VIDEOS_MUTED' in process.env) || process.env.VIDEOS_MUTED === 'true', // Automatically mute all videos in posts post_comments_sort: process.env.POST_COMMENTS_SORT || 'confidence', // "confidence" is the default sorting in Reddit. Must be one of: confidence, top, new, controversial, old, random, qa, live. reddit_app_id: process.env.REDDIT_APP_ID || 'ABfYqdDc9qPh1w', // If "use_reddit_oauth" config key is set to true, you have to obtain your Reddit app ID. For testing purposes it's okay to use this project's default app ID. Create your Reddit app here: https://old.reddit.com/prefs/apps/. Make sure to create an "installed app" type of app. domain_replacements: process.env.DOMAIN_REPLACEMENTS ? (JSON.parse(process.env.DOMAIN_REPLACEMENTS).map(([p, r]) => [new RegExp(p, 'gm'), r])) : [], // Replacements for domains in outgoing links. Tuples with regular expressions to match, and replacement values. This is in addition to user-level configuration of privacyDomains. + cache_control: !('CACHE_CONTROL' in process.env) || process.env.CACHE_CONTROL === 'true', // If true, teddit will automatically remove all cached static files. By default this is set to true. + cache_control_interval: process.env.CACHE_CONTROL_INTERVAL || 24, // How often the cache directory for static files is emptied (in hours). Requires cache_control to be true. Default is every 24 hours. + show_upvoted_percentage: !('SHOW_UPVOTED_PERCENTAGE' in process.env) || process.env.SHOW_UPVOTED_PERCENTAGE === 'true', + show_upvotes: !('SHOW_UPVOTES' in process.env) || process.env.SHOW_UPVOTES === 'true', // If true, teddit will show number of upvotes in posts and points in comments. post_media_max_heights: { /** * Sets the max-height value for images and videos in posts. @@ -65,9 +73,40 @@ const config = { initial_limit: 100, // This is the amount of page loads one IP address can make in one minute without getting limited. limit_after_limited: 30 // When an IP is limited, this is the amount of page loads the IP can make in one minute. }, - valid_media_domains: ['preview.redd.it', 'external-preview.redd.it', 'i.redd.it', 'v.redd.it', 'a.thumbs.redditmedia.com', 'b.thumbs.redditmedia.com', 'emoji.redditmedia.com', 'styles.redditmedia.com', 'www.redditstatic.com', 'thumbs.gfycat.com', 'i.ytimg.com'], + valid_media_domains: process.env.VALID_MEDIA_DOMAINS + ? JSON.parse(process.env.VALID_MEDIA_DOMAINS) + : ['preview.redd.it', 'external-preview.redd.it', 'i.redd.it', 'v.redd.it', 'a.thumbs.redditmedia.com', 'b.thumbs.redditmedia.com', 'emoji.redditmedia.com', 'styles.redditmedia.com', 'www.redditstatic.com', 'thumbs.gfycat.com', 'i.ytimg.com', 'i.imgur.com'], valid_embed_video_domains: ['gfycat.com', 'youtube.com'], - reddit_api_error_text: `Seems like your instance is either blocked (e.g. due to API rate limiting), reddit is currently down, or your API key is expired and not renewd properly. This can also happen for other reasons.` + reddit_api_error_text: `Seems like your instance is either blocked (e.g. due to API rate limiting), reddit is currently down, or your API key is expired and not renewd properly. This can also happen for other reasons.`, + /** + * Here you can configure the suggested subreddits which are visible in the + * cleaned homepage, and in the top bar. + * You should keep at least 'All', and 'Saved'. + * + * If you set your configs with an environment variables for example with + * docker-compose.yml, your suggested_subreddits config could be something + * like this (note the quotes): + * - SUGGESTED_SUBREDDITS=["Popular", "All", "Saved", "selfhosted", "linux", "datahoarder", "Monero"] + * or + * - 'SUGGESTED_SUBREDDITS=["Popular", "All", "Saved", "selfhosted", "linux", "datahoarder", "Monero"]' + */ + suggested_subreddits: process.env.SUGGESTED_SUBREDDITS + ? JSON.parse(process.env.SUGGESTED_SUBREDDITS) + : + ['Popular', 'All', 'Saved', 'AskReddit', 'pics', 'news', + 'worldnews', 'funny', 'tifu', 'videos', 'gaming', 'aww', + 'todayilearned', 'gifs', 'Art', 'explainlikeimfive', + 'movies', 'Jokes', 'TwoXChromosomes', + 'mildlyinteresting', 'LifeProTips', 'askscience', + 'IAmA', 'dataisbeautiful', 'books', 'science', + 'Showerthoughts', 'gadgets', 'Futurology', + 'nottheonion', 'history', 'sports', 'OldSchoolCool', + 'GetMotivated', 'DIY', 'photoshopbattles', 'nosleep', + 'Music', 'space', 'food', 'UpliftingNews', 'EarthPorn', + 'Documentaries', 'InternetIsBeautiful', + 'WritingPrompts', 'creepy', 'philosophy', + 'announcements', 'listentothis', 'blog'], }; module.exports = config; + diff --git a/docker-compose.yml b/docker-compose.yml index 317c21b1..93abedff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,16 @@ +# This docker-compose file is made for development purpose and build from source, if you want to use teddit in production, the README contains a production-ready docker-compose setup. + version: "3.8" services: - redis: - image: redis:6.0.9-alpine3.12 - command: redis-server - environment: - - REDIS_REPLICATION_MODE=master - ports: - - "6379:6379" - networks: - - teddit_net - web: + + teddit: + container_name: teddit build: . environment: - - REDIS_HOST=redis + - REDIS_HOST=teddit-redis ports: - - 8080:8080 + - "8080:8080" networks: - teddit_net healthcheck: @@ -23,6 +18,16 @@ services: interval: 1m timeout: 3s depends_on: - - redis + - teddit-redis + + teddit-redis: + container_name: teddit-redis + image: redis:6.2.5-alpine + command: redis-server + environment: + - REDIS_REPLICATION_MODE=master + networks: + - teddit_net + networks: teddit_net: diff --git a/inc/commons.js b/inc/commons.js index f6e3c286..aebc2e06 100644 --- a/inc/commons.js +++ b/inc/commons.js @@ -39,8 +39,10 @@ module.exports = function(request, fs) { this.teddifyUrl = (url, user_preferences) => { try { let u = new URL(url) + let domain_replaced = false if(u.host === 'www.reddit.com' || u.host === 'reddit.com') { url = url.replace(u.host, config.domain) + domain_replaced = true if(u.pathname.startsWith('/gallery/')) url = url.replace('/gallery/', '/comments/') } @@ -50,10 +52,15 @@ module.exports = function(request, fs) { let file_ext = getFileExtension(url) if(image_exts.includes(file_ext)) url = url.replace(`${u.host}/`, `${config.domain}/pics/w:null_`) + domain_replaced = true if(video_exts.includes(file_ext) || !image_exts.includes(file_ext)) url = url.replace(u.host, `${config.domain}/vids`) + '.mp4' + domain_replaced = true } + if(domain_replaced && !config.https_enabled) { + url = url.replace('https:', 'http:') + } } catch(e) { } url = replaceDomains(url, user_preferences) return url @@ -182,28 +189,113 @@ module.exports = function(request, fs) { this.replaceUserDomains = (str, user_preferences) => { - let redditRegex = /([A-z.]+\.)?(reddit(\.com)|redd(\.it))/gm; - let youtubeRegex = /([A-z.]+\.)?youtu(be\.com|\.be)/gm; - let twitterRegex = /([A-z.]+\.)?twitter\.com/gm; - let instagramRegex = /([A-z.]+\.)?instagram.com/gm; + let redditRegex = /(?<=href=")(https?:\/\/)([A-z.]+\.)?(reddit(\.com)|redd(\.it))(?=.+")/gm; + let youtubeRegex = /(?<=href=")(https?:\/\/)([A-z.]+\.)?youtu(be\.com|\.be)(?=.+")/gm; + let twitterRegex = /(?<=href=")(https?:\/\/)(www\.)?twitter\.com(?=.+")/gm; + let instagramRegex = /(?<=href=")(https?:\/\/)(www+\.)?instagram.com(?=.+")/gm; + let quoraRegex = /(?<=href=")(https?:\/\/)([A-z.]+\.)?quora\.com(?=.+")/gm; - str = str.replace(redditRegex, config.domain) + /* + * regex pattern to replace imgur links (imgur.com, imgur.io, i.stack.imgur.com) + * source: https://github.com/libredirect/libredirect/blob/32c4a0211e3b721d46219c05cba93f1a42cf3773/src/config/config.json#L317 + * license: GNU GPL v3 License -> https://github.com/libredirect/libredirect/blob/32c4a0211e3b721d46219c05cba93f1a42cf3773/LICENSE + */ + let imgurRegex = /(?<=href=")(https?:\/{2})([im]\.)?(stack\.)?imgur\.(com|io)(?=.+")/gm; + + let protocol = config.https_enabled || config.api_force_https ? 'https://' : 'http://' + + /** + * Special handling for reddit media domains in comments hrefs or img srcs. + * For example a comment might have a direct links to images in i.redd.it: + * Just refer to this + * We want to rewrite these hrefs, but we also need to include the domain + * for our backend, so we know where to fetch the media from. + * That comment URL then becomes like this after rewriting it: + * Just refer to this + * And then in our backend, we check if we have a 'teddit_proxy' in the req + * query, and proceed to proxy if it does. + */ + const replacable_media_domains = ['i.redd.it', 'v.redd.it', 'external-preview.redd.it', 'preview.redd.it'] + replacable_media_domains.forEach((domain) => { + if (str.includes(domain + "/")) { + const regex = new RegExp(`(?<=(href|src)=")(https?:\/\/)([A-z.]+\.)?(${domain})(.+?(?="))`, 'gm') + const hrefs = str.match(regex) + if (!hrefs) { + return + } + + hrefs.forEach((url) => { + let original_url = url + const valid_exts = ['png', 'jpg', 'jpeg', 'mp4', 'gif', 'gifv'] + const file_ext = getFileExtension(url) + if (valid_exts.includes(file_ext)) { + url = url.replace(domain, config.domain) + + // append the domain info to the query, for teddit backend + let u = new URL(url) + if (u.search) { + url += '&teddit_proxy=' + domain + } else { + url += '?teddit_proxy=' + domain + } + + // also replace the protocol for instances using http only + if (protocol === 'http://' && u.protocol === 'https:') { + url.replace('https://', protocol) + } + str = str.replace(original_url, url) + } + }) + } + }) + + // Continue the normal replace logic + + str = str.replace(redditRegex, protocol + config.domain) if(typeof(user_preferences) == 'undefined') return str if(typeof(user_preferences.domain_youtube) != 'undefined') - if(user_preferences.domain_youtube) - str = str.replace(youtubeRegex, user_preferences.domain_youtube) + if(user_preferences.domain_youtube){ + if (!youtubeRegex.test(str)){ + youtubeRegex = /(https?:\/\/)([A-z.]+\.)?youtu(be\.com|\.be)(?=.+)/gm; + } + str = str.replace(youtubeRegex, protocol + user_preferences.domain_youtube); + } if(typeof(user_preferences.domain_twitter) != 'undefined') - if(user_preferences.domain_twitter) - str = str.replace(twitterRegex, user_preferences.domain_twitter) - - if(typeof(user_preferences.domain_instagram) != 'undefined') - if(user_preferences.domain_instagram) - str = str.replace(instagramRegex, user_preferences.domain_instagram) + if(user_preferences.domain_twitter){ + if (!twitterRegex.test(str)){ + twitterRegex = /(https?:\/\/)(www\.)?twitter\.com(?=.)/gm; + } + str = str.replace(twitterRegex, protocol + user_preferences.domain_twitter) + } + if(typeof(user_preferences.domain_instagram) != 'undefined'){ + if(user_preferences.domain_instagram){ + if (!instagramRegex.test(str)){ + instagramRegex = /(https?:\/\/)(www+\.)?instagram.com(?=.)/gm; + } + str = str.replace(instagramRegex, protocol + user_preferences.domain_instagram); + } + } + if(typeof(user_preferences.domain_quora) != 'undefined'){ + if(user_preferences.domain_quora){ + if (!quoraRegex.test(str)){ + quoraRegex = /(https?:\/\/)([A-z.]+\.)?quora\.com(?=.)/gm; + } + str = str.replace(quoraRegex, protocol + user_preferences.domain_quora) + } + } + if(typeof(user_preferences.domain_imgur) != 'undefined'){ + if(user_preferences.domain_imgur){ + if (!imgurRegex.test(str)){ + imgurRegex = /(https?:\/{2})([im]\.)?(stack\.)?imgur\.(com|io)(?=.)/gm; + } + str = str.replace(imgurRegex, protocol + user_preferences.domain_imgur) + } + } return str } diff --git a/inc/compilePostComments.js b/inc/compilePostComments.js index 0fecde62..2eb09895 100644 --- a/inc/compilePostComments.js +++ b/inc/compilePostComments.js @@ -5,10 +5,14 @@ module.exports = function() { let comments_html function commentAuthor(comment, classlist, submitter, moderator) { let classes = classlist.join(' ') - if (comment.author === '[deleted]') - return `[deleted]` - else + if (comment.author === '[deleted]') { + var reveddit_url = "https://www.reveddit.com" + post_url.substr(post_url.indexOf('/r/')) + comments.id + return `[deleted]` + } + + else { return `${comment.author}${submitter || ''}${moderator || ''}` + } } if(!user_preferences) @@ -115,6 +119,7 @@ module.exports = function() { } } else { let link = comments.parent_id.split('_')[1] + link = post_url + link comments_html = `
continue this thread @@ -205,7 +210,7 @@ module.exports = function() { } comments_html += replies_html + '
' } else { - if(comment.children.length > 0) { + if(comment.children.length > 0) { let parent_id = comment.parent_id.split('_')[1] let load_comms_href = parent_id @@ -215,7 +220,8 @@ module.exports = function() { ` } else { - let link = comment.parent_id.split('_')[1] + let link = comment.parent_id.split('_')[1] + link = post_url + link comments_html = `
continue this thread diff --git a/inc/components/link.js b/inc/components/link.js new file mode 100644 index 00000000..99312626 --- /dev/null +++ b/inc/components/link.js @@ -0,0 +1,88 @@ +/* + * Corresponds to `components/link.pug` + */ + +const config = require('../../config') +let valid_reddit_self_domains = ['reddit.com'] + +// Parses a link from a response returned by reddit. +async function fromJson(data, user_preferences, subreddit_front) { + let result = {} + + // Meta + result.id = data.id + result.permalink = data.permalink + result.created = data.created_utc + result.author = data.author + result.title = data.title + result.over_18 = data.over_18 + result.score = data.score + result.ups = data.ups + result.upvote_ratio = data.upvote_ratio + result.num_comments = data.num_comments + + // Content + result.is_self_link = false + result.selftext_html = data.selftext_html + result.url = replaceDomains(data.url, user_preferences) + result.domain = data.domain + result.is_video = data.is_video + result.media = data.media + result.duration = null + result.images = null + + if(data.is_video && data.media) { + if(data.media.reddit_video) { + result.duration = data.media.reddit_video.duration + } + } + + // Moderation attributes + result.locked = data.locked + result.stickied = data.stickied + + // Subreddit + result.subreddit_front = subreddit_front + result.subreddit = data.subreddit + + // Flair + result.link_flair = (user_preferences.flairs != 'false' ? await formatLinkFlair(data) : '') + result.user_flair = (user_preferences.flairs != 'false' ? await formatUserFlair(data) : '') + result.link_flair_text = data.link_flair_text + + if(data.domain) { + let tld = data.domain.split('self.') + if(tld.length > 1) { + if(!tld[1].includes('.')) { + result.is_self_link = true + } + } + if(config.valid_media_domains.includes(data.domain) || valid_reddit_self_domains.includes(data.domain)) { + result.is_self_link = true + } + } + + if(data.preview && data.thumbnail !== 'self') { + if(!data.url.startsWith('/r/') && isGif(data.url)) { + result.images = { + thumb: await downloadAndSave(data.thumbnail, 'thumb_') + } + } else { + if(data.preview.images[0].resolutions[0]) { + let preview = null + if(!isGif(data.url) && !data.post_hint.includes(':video')) + preview = await downloadAndSave(data.preview.images[0].source.url) + result.images = { + thumb: await downloadAndSave(data.preview.images[0].resolutions[0].url, 'thumb_'), + preview: preview + } + } + } + } + + return result +} + +module.exports = { + fromJson, +} diff --git a/inc/initRedditApi.js b/inc/initRedditApi.js index 2824fca5..e3cbe7de 100644 --- a/inc/initRedditApi.js +++ b/inc/initRedditApi.js @@ -30,7 +30,7 @@ module.exports = function(fetch) { } else { console.error(`Something went wrong while trying to get an access token from reddit API. ${result.status} – ${result.statusText}`) console.error(reddit_api_error_text) - return res.render('index', { json: null, http_status_code: result.status }) + return res.render('frontpage', { json: null, http_status_code: result.status, instance_config: config }) } }).catch(error => { console.log(`Error while obtaining a reddit API key.`, error) @@ -66,19 +66,22 @@ module.exports = function(fetch) { } else { console.error(`Something went wrong while fetching data from reddit API. ${result.status} – ${result.statusText}`) console.error(reddit_api_error_text) - return res.render('index', { json: null, http_status_code: result.status }) + return res.render('frontpage', { json: null, http_status_code: result.status, instance_config: config }) } }).catch(error => { console.log(`Error while refreshing the reddit API key.`, error) }) } this.redditApiGETHeaders = function() { + let cookies = `edgebucket=; _options={%22pref_gated_sr_optin%22:true,%22pref_quarantine_optin%22:true}` + if(!config.use_reddit_oauth) - return { method: 'GET' } + return { headers: { cookie: cookies }, method: 'GET' } return { headers: { - Authorization: `Bearer ${reddit_access_token}` + Authorization: `Bearer ${reddit_access_token}`, + cookie: cookies }, method: 'GET' } diff --git a/inc/processJsonPost.js b/inc/processJsonPost.js index ef166649..7a8ccc80 100644 --- a/inc/processJsonPost.js +++ b/inc/processJsonPost.js @@ -1,155 +1,59 @@ -module.exports = function(fetch) { - var compilePostComments = require('./compilePostComments.js')(); - var procPostMedia = require('./processPostMedia.js')(); - this.processJsonPost = (json, parsed, user_preferences) => { - return new Promise(resolve => { - (async () => { - if(!parsed) { - json = JSON.parse(json) - } - - let post = json[0].data.children[0].data - let post_id = post.name - let comments = json[1].data.children - - let obj = { - author: post.author, - created: post.created_utc, - edited: post.edited, - is_video: post.is_video, - locked: post.locked, - link_flair_text: post.link_flair_text, - name: post_id, - num_comments: post.num_comments, - over_18: post.over_18, - permalink: teddifyUrl(post.permalink), - title: post.title, - url: teddifyUrl(post.url, user_preferences), - ups: post.ups, - id: post.id, - domain: post.domain, - contest_mode: post.contest_mode, - upvote_ratio: post.upvote_ratio, - comments: null, - has_media: false, - media: null, - images: null, - crosspost: false, - selftext: unescape(post.selftext_html), - poll_data: post.poll_data, - link_flair: (user_preferences.flairs != 'false' ? await formatLinkFlair(post) : ''), - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(post) : '') - } - - let valid_embed_video_domains = ['gfycat.com'] - let has_gif = false - let gif_to_mp4 = null - let reddit_video = null - let embed_video = false - - if(post.media) - if(valid_embed_video_domains.includes(post.media.type)) - embed_video = true - - if(post.preview && !embed_video) { - if(post.preview.reddit_video_preview) { - if(post.preview.reddit_video_preview.is_gif) { - has_gif = true - gif_url = post.preview.reddit_video_preview.fallback_url - } else { - let file_ext = getFileExtension(post.preview.reddit_video_preview.fallback_url) - if(file_ext === 'mp4')  { - post.media = true - reddit_video = post.preview.reddit_video_preview - } - } - } - if(post.preview.images) { - if(post.preview.images[0].source) { - let file_ext = getFileExtension(post.preview.images[0].source.url) - if(file_ext === 'gif') { - has_gif = true - let resolutions = post.preview.images[0].variants.mp4.resolutions - gif_to_mp4 = resolutions[resolutions.length - 1] - } - } - } - } - - obj = await processPostMedia(obj, post, post.media, has_gif, reddit_video, gif_to_mp4) - - if(post.crosspost_parent_list) { - post.crosspost = post.crosspost_parent_list[0] - } - if(post.crosspost) { - obj = await processPostMedia(obj, post.crosspost, post.crosspost.media, has_gif, reddit_video, gif_to_mp4) - obj.crosspost = { - author: post.crosspost.author, - created: post.crosspost.created_utc, - subreddit: post.crosspost.subreddit, - title: post.crosspost.title, - name: post.crosspost.name, - num_comments: post.crosspost.num_comments, - over_18: post.crosspost.over_18, - id: post.crosspost.id, - permalink: teddifyUrl(post.crosspost.permalink), - ups: post.crosspost.ups, - selftext: unescape(post.selftext_html), - selftext_crosspost: unescape(post.crosspost.selftext_html), - poll_data: post.poll_data, - is_crosspost: true, - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(post) : '') - } - } +const compilePostComments = require('./compilePostComments.js')(); +const procPostMedia = require('./processPostMedia.js')(); +const config = require('../config'); - if(post.preview && !obj.has_media) { - obj.images = { - source: await downloadAndSave(post.preview.images[0].source.url) - } - } - - if(obj.media) { - if(obj.media.source === 'external') { - if(post.preview) { - obj.images = { - source: await downloadAndSave(post.preview.images[0].source.url) - } - } - } - } - - if(post.gallery_data) { - obj.gallery = true - obj.gallery_items = [] - for(var i = 0; i < post.gallery_data.items.length; i++) { - let id = post.gallery_data.items[i].media_id - if(post.media_metadata[id]) { - if(post.media_metadata[id].p) { - if(post.media_metadata[id].p[0]) { - let item = { source: null, thumbnail: null, large: null } - if(post.media_metadata[id].s && post.media_metadata[id].p[0].u) { - item = { - type: post.media_metadata[id].e, - source: await downloadAndSave(post.media_metadata[id].s.u), - thumbnail: await downloadAndSave(post.media_metadata[id].p[0].u), - large: await downloadAndSave(post.media_metadata[id].p[post.media_metadata[id].p.length - 1].u), - } - } - obj.gallery_items.push(item) - } - } - } - } - } +async function processReplies(data, post_id, depth, user_preferences) { + let return_replies = []; + for (var i = 0; i < data.length; i++) { + let kind = data[i].kind; + let reply = data[i].data; + let obj = {}; + if (kind !== 'more') { + obj = { + author: reply.author, + body_html: reply.body_html, + parent_id: reply.parent_id, + created: reply.created_utc, + edited: reply.edited, + score: reply.score, + ups: reply.ups, + id: reply.id, + permalink: teddifyUrl(reply.permalink), + stickied: reply.stickied, + distinguished: reply.distinguished, + score_hidden: reply.score_hidden, + edited: reply.edited, + replies: [], + depth: depth, + user_flair: + user_preferences.flairs != 'false' + ? await formatUserFlair(reply) + : '', + controversiality: + user_preferences.highlight_controversial != 'false' + ? reply.controversiality + : '', + }; + } else { + obj = { + type: 'load_more', + count: reply.count, + id: reply.id, + parent_id: reply.parent_id, + post_id: post_id, + children: [], + depth: depth, + }; + } - let comms = [] - for(var i = 0; i < comments.length; i++) { - let comment = comments[i].data - let kind = comments[i].kind - let obj = {} + if (reply.replies && kind !== 'more') { + if (reply.replies.data.children.length) { + for (var j = 0; j < reply.replies.data.children.length; j++) { + let comment = reply.replies.data.children[j].data; + let objct = {}; - if(kind !== 'more') { - obj = { + if (comment.author && comment.body_html) { + objct = { author: comment.author, body_html: comment.body_html, parent_id: comment.parent_id, @@ -159,174 +63,478 @@ module.exports = function(fetch) { ups: comment.ups, id: comment.id, permalink: teddifyUrl(comment.permalink), - stickied: comment.stickied, - distinguished: comment.distinguished, score_hidden: comment.score_hidden, - edited: comment.edited, + distinguished: comment.distinguished, + distinguished: comment.edited, replies: [], - depth: comment.depth, - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(comment) : ''), - controversiality: (user_preferences.highlight_controversial != 'false' ? comment.controversiality : '') - } + depth: depth + 1, + user_flair: + user_preferences.flairs != 'false' + ? await formatUserFlair(comment) + : '', + controversiality: + user_preferences.highlight_controversial != 'false' + ? comment.controversiality + : '', + }; } else { - obj = { + objct = { type: 'load_more', count: comment.count, id: comment.id, parent_id: comment.parent_id, - post_id: post.name, - children: [] - } - } - - if(comment.replies && kind !== 'more') { - if(comment.replies.data) { - if(comment.replies.data.children.length > 0) { - obj.replies = await processReplies(comment.replies.data.children, post_id, 1, user_preferences) + post_id: post_id, + children: [], + depth: depth + 1, + }; + if (comment.children) { + for (var k = 0; k < comment.children.length; k++) { + objct.children.push(comment.children[k]); } } } - if(comment.children) { - for(var j = 0; j < comment.children.length; j++) { - obj.children.push(comment.children[j]) + if (comment.replies) { + if (comment.replies.data) { + if (comment.replies.data.children.length > 0) { + objct.replies = await processReplies( + comment.replies.data.children, + post_id, + depth, + user_preferences + ); + } } } - comms.push(obj) + obj.replies.push(objct); } + } + } - obj.comments = comms + if (reply.children) { + for (var j = 0; j < reply.children.length; j++) { + obj.children.push(reply.children[j]); + } + } - resolve(obj) - })() - }) + return_replies.push(obj); } + return return_replies; +} + +async function processJsonPost(json, parsed, user_preferences) { + if (!parsed) { + json = JSON.parse(json); + } + + let post = json[0].data.children[0].data; + let post_id = post.name; + let comments = json[1].data.children; + + let obj = { + author: post.author, + created: post.created_utc, + edited: post.edited, + is_video: post.is_video, + locked: post.locked, + link_flair_text: post.link_flair_text, + name: post_id, + num_comments: post.num_comments, + over_18: post.over_18, + permalink: teddifyUrl(post.permalink), + title: post.title, + url: teddifyUrl(post.url, user_preferences), + ups: post.ups, + id: post.id, + domain: post.domain, + contest_mode: post.contest_mode, + upvote_ratio: post.upvote_ratio, + comments: null, + has_media: false, + media: null, + images: null, + crosspost: false, + selftext: unescape(post.selftext_html), + selftext_preview: post.selftext.substr(0, 120).replace(/\n/g, ' '), + poll_data: post.poll_data, + link_flair: + user_preferences.flairs != 'false' ? await formatLinkFlair(post) : '', + user_flair: + user_preferences.flairs != 'false' ? await formatUserFlair(post) : '', + }; - this.finalizeJsonPost = async (processed_json, post_id, post_url, morechildren_ids, viewing_comment, user_preferences) => { - let comments_html = `
` - let comments = processed_json.comments - let last_known_depth = undefined - for(var i = 0; i < comments.length; i++) { - let next_comment = false - if(comments[i+1]) { - next_comment = comments[i+1] + let valid_embed_video_domains = ['gfycat.com']; + let has_gif = false; + let gif_to_mp4 = null; + let reddit_video = null; + let embed_video = false; + + if (post.media) + if (valid_embed_video_domains.includes(post.media.type)) embed_video = true; + + if (post.preview && !embed_video) { + if (post.preview.reddit_video_preview) { + if (post.preview.reddit_video_preview.is_gif) { + has_gif = true; + gif_url = post.preview.reddit_video_preview.fallback_url; + } else { + let file_ext = getFileExtension( + post.preview.reddit_video_preview.fallback_url + ); + if (file_ext === 'mp4') { + post.media = true; + reddit_video = post.preview.reddit_video_preview; + } } - if(comments[i].depth != undefined) { - last_known_depth = comments[i].depth + } + if (post.preview.images) { + if (post.preview.images[0].source) { + let file_ext = getFileExtension(post.preview.images[0].source.url); + if (file_ext === 'gif') { + has_gif = true; + let resolutions = post.preview.images[0].variants.mp4.resolutions; + gif_to_mp4 = resolutions[resolutions.length - 1]; + } } - - comments_html += await compilePostCommentsHtml(comments[i], next_comment, post_id, post_url, morechildren_ids, processed_json.author, viewing_comment, user_preferences, last_known_depth) } + } - comments_html += `
` + obj = await processPostMedia( + obj, + post, + post.media, + has_gif, + reddit_video, + gif_to_mp4 + ); - delete processed_json['comments'] - let post_data = processed_json - return { post_data: post_data, comments: comments_html } + if (post.crosspost_parent_list) { + post.crosspost = post.crosspost_parent_list[0]; + } + if (post.crosspost) { + obj = await processPostMedia( + obj, + post.crosspost, + post.crosspost.media, + has_gif, + reddit_video, + gif_to_mp4 + ); + obj.crosspost = { + author: post.crosspost.author, + created: post.crosspost.created_utc, + subreddit: post.crosspost.subreddit, + title: post.crosspost.title, + name: post.crosspost.name, + num_comments: post.crosspost.num_comments, + over_18: post.crosspost.over_18, + id: post.crosspost.id, + permalink: teddifyUrl(post.crosspost.permalink), + ups: post.crosspost.ups, + selftext: unescape(post.selftext_html), + selftext_crosspost: unescape(post.crosspost.selftext_html), + poll_data: post.poll_data, + is_crosspost: true, + user_flair: + user_preferences.flairs != 'false' ? await formatUserFlair(post) : '', + }; } - this.processReplies = async (data, post_id, depth, user_preferences) => { - let return_replies = [] - for(var i = 0; i < data.length; i++) { - let kind = data[i].kind - let reply = data[i].data - let obj = {} - if(kind !== 'more') { - obj = { - author: reply.author, - body_html: reply.body_html, - parent_id: reply.parent_id, - created: reply.created_utc, - edited: reply.edited, - score: reply.score, - ups: reply.ups, - id: reply.id, - permalink: teddifyUrl(reply.permalink), - stickied: reply.stickied, - distinguished: reply.distinguished, - score_hidden: reply.score_hidden, - edited: reply.edited, - replies: [], - depth: depth, - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(reply) : ''), - controversiality: (user_preferences.highlight_controversial != 'false' ? reply.controversiality : '') - } - } else { - obj = { - type: 'load_more', - count: reply.count, - id: reply.id, - parent_id: reply.parent_id, - post_id: post_id, - children: [], - depth: depth - } + if (post.preview && !obj.has_media) { + obj.images = { + source: await downloadAndSave(post.preview.images[0].source.url), + }; + } + + if (obj.media) { + if (obj.media.source === 'external') { + if (post.preview) { + obj.images = { + source: await downloadAndSave(post.preview.images[0].source.url), + }; } + } + } - if(reply.replies && kind !== 'more') { - if(reply.replies.data.children.length) { - for(var j = 0; j < reply.replies.data.children.length; j++) { - let comment = reply.replies.data.children[j].data - let objct = {} - - if(comment.author && comment.body_html) { - objct = { - author: comment.author, - body_html: comment.body_html, - parent_id: comment.parent_id, - created: comment.created_utc, - edited: comment.edited, - score: comment.score, - ups: comment.ups, - id: comment.id, - permalink: teddifyUrl(comment.permalink), - score_hidden: comment.score_hidden, - distinguished: comment.distinguished, - distinguished: comment.edited, - replies: [], - depth: depth + 1, - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(comment) : ''), - controversiality: (user_preferences.highlight_controversial != 'false' ? comment.controversiality : '') - } - } else { - objct = { - type: 'load_more', - count: comment.count, - id: comment.id, - parent_id: comment.parent_id, - post_id: post_id, - children: [], - depth: depth + 1 - } - if(comment.children) { - for(var k = 0; k < comment.children.length; k++) { - objct.children.push(comment.children[k]) - } + if (post.gallery_data) { + obj.gallery = true; + obj.gallery_items = []; + for (var i = 0; i < post.gallery_data.items.length; i++) { + let id = post.gallery_data.items[i].media_id; + if (post.media_metadata) { + if (post.media_metadata[id]) { + if (post.media_metadata[id].p) { + if (post.media_metadata[id].p[0]) { + let item = { source: null, thumbnail: null, large: null }; + if (post.media_metadata[id].s && post.media_metadata[id].p[0].u) { + item = { + type: post.media_metadata[id].e, + source: await downloadAndSave(post.media_metadata[id].s.u), + thumbnail: await downloadAndSave( + post.media_metadata[id].p[0].u + ), + large: await downloadAndSave( + post.media_metadata[id].p[ + post.media_metadata[id].p.length - 1 + ].u + ), + caption: post.gallery_data.items[i].caption || false, + }; } + obj.gallery_items.push(item); } + } + } + } + } + } - if(comment.replies) { - if(comment.replies.data) { - if(comment.replies.data.children.length > 0) { - objct.replies = await processReplies(comment.replies.data.children, post_id, depth, user_preferences) - } - } - } + let comms = []; + for (var i = 0; i < comments.length; i++) { + let comment = comments[i].data; + let kind = comments[i].kind; + let obj = {}; - obj.replies.push(objct) - } + if (kind !== 'more') { + obj = { + author: comment.author, + body_html: comment.body_html, + parent_id: comment.parent_id, + created: comment.created_utc, + edited: comment.edited, + score: comment.score, + ups: comment.ups, + id: comment.id, + permalink: teddifyUrl(comment.permalink), + stickied: comment.stickied, + distinguished: comment.distinguished, + score_hidden: comment.score_hidden, + edited: comment.edited, + replies: [], + depth: comment.depth, + user_flair: + user_preferences.flairs != 'false' + ? await formatUserFlair(comment) + : '', + controversiality: + user_preferences.highlight_controversial != 'false' + ? comment.controversiality + : '', + }; + } else { + obj = { + type: 'load_more', + count: comment.count, + id: comment.id, + parent_id: comment.parent_id, + post_id: post.name, + children: [], + }; + } + + if (comment.replies && kind !== 'more') { + if (comment.replies.data) { + if (comment.replies.data.children.length > 0) { + obj.replies = await processReplies( + comment.replies.data.children, + post_id, + 1, + user_preferences + ); } } + } + + if (comment.children) { + for (var j = 0; j < comment.children.length; j++) { + obj.children.push(comment.children[j]); + } + } + + comms.push(obj); + } + + obj.comments = comms; + + return obj; +} + +async function finalizeJsonPost( + processed_json, + post_id, + post_url, + morechildren_ids, + viewing_comment, + user_preferences +) { + let comments_html = `
`; + let comments = processed_json.comments; + let last_known_depth = undefined; + for (var i = 0; i < comments.length; i++) { + let next_comment = false; + if (comments[i + 1]) { + next_comment = comments[i + 1]; + } + if (comments[i].depth != undefined) { + last_known_depth = comments[i].depth; + } + + comments_html += await compilePostCommentsHtml( + comments[i], + next_comment, + post_id, + post_url, + morechildren_ids, + processed_json.author, + viewing_comment, + user_preferences, + last_known_depth + ); + } + + comments_html += `
`; + + delete processed_json['comments']; + let post_data = processed_json; + return { post_data: post_data, comments: comments_html }; +} + +async function processJsonPostList(posts, mode) { + let protocol = config.https_enabled || config.api_force_https ? 'https' : 'http'; + + for (var i = 0; i < posts.length; i++) { + let link = posts[i]; + let valid_reddit_self_domains = ['reddit.com']; + let is_self_link = false; - if(reply.children) { - for(var j = 0; j < reply.children.length; j++) { - obj.children.push(reply.children[j]) + if (link.domain) { + let tld = link.domain.split('self.'); + if (tld.length > 1) { + if (!tld[1].includes('.')) { + is_self_link = true; + link.url = teddifyUrl(link.url); } } + if ( + config.valid_media_domains.includes(link.domain) || + valid_reddit_self_domains.includes(link.domain) + ) { + is_self_link = true; + link.url = teddifyUrl(link.url); + } + } + + link.permalink = `${protocol}://${config.domain}${link.permalink}`; + + if (is_self_link) link.url = link.permalink; + + if (link.images) { + if (link.images.thumb !== 'self') { + link.images.thumb = `${protocol}://${config.domain}${link.images.thumb}`; + } + } + + if (mode === 'light') { + link.selftext_html = null; + } + } + + return posts; +} + +async function getPostItem(post_json, req, protocol) { + let thumbnail = ''; + let post_image = ''; + let is_self_link = false; + let valid_reddit_self_domains = ['reddit.com']; + + if (post_json.domain) { + let tld = post_json.domain.split('self.'); + if (tld.length > 1) { + if (!tld[1].includes('.')) { + is_self_link = true; + post_json.url = teddifyUrl(post_json.url); + } + } + if ( + config.valid_media_domains.includes(post_json.domain) || + valid_reddit_self_domains.includes(post_json.domain) + ) { + is_self_link = true; + post_json.url = teddifyUrl(post_json.url); + } + } - return_replies.push(obj) + if (post_json.preview && post_json.thumbnail !== 'self') { + if (!post_json.url.startsWith('/r/') && isGif(post_json.url)) { + let s = await downloadAndSave(post_json.thumbnail, 'thumb_'); + thumbnail = `${protocol}://${config.domain}${s}`; + } else { + if (post_json.preview.images[0].resolutions[0]) { + let s = await downloadAndSave( + post_json.preview.images[0].resolutions[0].url, + 'thumb_' + ); + thumbnail = `${protocol}://${config.domain}${s}`; + if (!isGif(post_json.url) && !post_json.post_hint.includes(':video')) { + s = await downloadAndSave(post_json.preview.images[0].source.url); + post_image = `${protocol}://${config.domain}${s}`; + } + } } - return return_replies } + + post_json.permalink = `${protocol}://${config.domain}${post_json.permalink}`; + + if (is_self_link) post_json.url = post_json.permalink; + + if (req.query.hasOwnProperty('full_thumbs')) { + if (!post_image) post_image = thumbnail; + + thumbnail = post_image; + } + + let enclosure = ''; + if (thumbnail != '') { + let mime = ''; + let ext = thumbnail.split('.').pop(); + if (ext === 'png') mime = 'image/png'; + else mime = 'image/jpeg'; + enclosure = ``; + } + + let append_desc_html = `
[link] [comments]`; + + return ` + + ${post_json.title} + ${post_json.author} + ${post_json.created} + ${new Date( + post_json.created_utc * 1000 + ).toGMTString()} + ${post_json.domain} + ${post_json.id} + ${thumbnail} + ${enclosure} + ${post_json.permalink} + ${post_json.url} + + ${post_json.num_comments} + ${post_json.ups} + ${post_json.stickied} + ${is_self_link} + + `; } + +module.exports = { + processReplies, + processJsonPost, + finalizeJsonPost, + processJsonPostList, + getPostItem +}; diff --git a/inc/processJsonSubreddit.js b/inc/processJsonSubreddit.js index 597033e6..8d8f57fb 100644 --- a/inc/processJsonSubreddit.js +++ b/inc/processJsonSubreddit.js @@ -1,108 +1,90 @@ -module.exports = function() { - const config = require('../config'); - this.processJsonSubreddit = (json, from, subreddit_front, user_preferences, saved) => { - return new Promise(resolve => { - (async () => { - if(from === 'redis') { - json = JSON.parse(json) - } - if(json.error) { - resolve({ error: true, error_data: json }) - } else { - if(saved) { - let t = { - data: { - before: null, - after: null, - children: json - } - } - json = t - } - - let before = json.data.before - let after = json.data.after +const config = require('../config'); +const link = require('./components/link'); - let ret = { - info: { - before: before, - after: after - }, - links: [] - } +async function processJsonSubreddit( + json, + from, + subreddit_front, + user_preferences, + saved +) { + if (from === 'redis') { + json = JSON.parse(json); + } + if (json.error) { + return { error: true, error_data: json }; + } else { + if (saved) { + let t = { + data: { + before: null, + after: null, + children: json, + }, + }; + json = t; + } + + let before = json.data.before; + let after = json.data.after; + + let ret = { + info: { + before: before, + after: after, + }, + links: [], + }; - let children_len = json.data.children.length + let children_len = json.data.children.length; - for(var i = 0; i < children_len; i++) { - let data = json.data.children[i].data - let images = null - let is_self_link = false - let valid_reddit_self_domains = ['reddit.com'] + for (var i = 0; i < children_len; i++) { + let data = json.data.children[i].data; - if(data.over_18) - if((config.nsfw_enabled === false && user_preferences.nsfw_enabled != 'true') || user_preferences.nsfw_enabled === 'false') - continue + if (data.over_18) + if ( + (config.nsfw_enabled === false && + user_preferences.nsfw_enabled != 'true') || + user_preferences.nsfw_enabled === 'false' + ) + continue; - if(data.domain) { - let tld = data.domain.split('self.') - if(tld.length > 1) { - if(!tld[1].includes('.')) { - is_self_link = true - } - } - if(config.valid_media_domains.includes(data.domain) || valid_reddit_self_domains.includes(data.domain)) { - is_self_link = true - } - } + /* + // Todo: Remove this once the link component is done + // but keep it for now in case we need it later + let obj = { + author: data.author, + created: data.created_utc, + domain: data.domain, + id: data.id, + images: images, + is_video: data.is_video, + link_flair_text: data.link_flair_text, + locked: data.locked, + media: data.media, + selftext_html: data.selftext_html, + num_comments: data.num_comments, + over_18: data.over_18, + permalink: data.permalink, + score: data.score, + subreddit: data.subreddit, + title: data.title, + ups: data.ups, + upvote_ratio: data.upvote_ratio, + url: replaceDomains(data.url, user_preferences), + stickied: data.stickied, + is_self_link: is_self_link, + subreddit_front: subreddit_front, + link_flair: (user_preferences.flairs != 'false' ? await formatLinkFlair(data) : ''), + user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(data) : '') + } */ - if(data.preview && data.thumbnail !== 'self') { - if(!data.url.startsWith('/r/') && isGif(data.url)) { - images = { - thumb: await downloadAndSave(data.thumbnail, 'thumb_') - } - } else { - if(data.preview.images[0].resolutions[0]) { - let preview = null - if(!isGif(data.url) && !data.post_hint.includes(':video')) - preview = await downloadAndSave(data.preview.images[0].source.url) - images = { - thumb: await downloadAndSave(data.preview.images[0].resolutions[0].url, 'thumb_'), - preview: preview - } - } - } - } - let obj = { - author: data.author, - created: data.created_utc, - domain: data.domain, - id: data.id, - images: images, - is_video: data.is_video, - link_flair_text: data.link_flair_text, - locked: data.locked, - media: data.media, - selftext_html: data.selftext_html, - num_comments: data.num_comments, - over_18: data.over_18, - permalink: data.permalink, - score: data.score, - subreddit: data.subreddit, - title: data.title, - ups: data.ups, - upvote_ratio: data.upvote_ratio, - url: replaceDomains(data.url, user_preferences), - stickied: data.stickied, - is_self_link: is_self_link, - subreddit_front: subreddit_front, - link_flair: (user_preferences.flairs != 'false' ? await formatLinkFlair(data) : ''), - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(data) : '') - } - ret.links.push(obj) - } - resolve(ret) - } - })() - }) + let obj = await link.fromJson(data, user_preferences, subreddit_front); + + ret.links.push(obj); + } + return ret; } } + +module.exports = processJsonSubreddit; diff --git a/inc/processJsonUser.js b/inc/processJsonUser.js index 2313ed1d..7e76585d 100644 --- a/inc/processJsonUser.js +++ b/inc/processJsonUser.js @@ -1,119 +1,138 @@ -module.exports = function() { - const config = require('../config'); - this.processJsonUser = function(json, parsed, after, before, user_preferences, kind, post_type) { - return new Promise(resolve => { - (async () => { - if(!parsed) { - json = JSON.parse(json) - } - - let about = json.about.data - let posts = [] - let view_more_posts = false - let posts_limit = 25 - let user_front = false - - if(json.overview.data.children.length > posts_limit) { - view_more_posts = true - } else { - posts_limit = json.overview.data.children.length - } - - if(!after && !before) { - user_front = true - } - - if(json.overview.data.children) { - if(json.overview.data.children[posts_limit - 1]) { - after = json.overview.data.children[posts_limit - 1].data.name - } - if(json.overview.data.children[0]) { - before = json.overview.data.children[0].data.name - } - } - - for(var i = 0; i < posts_limit; i++) { - let post = json.overview.data.children[i].data - let thumbnail = 'self' - let type = json.overview.data.children[i].kind - let obj - - let post_id = post.permalink.split('/').slice(-2)[0] + '/' - let url = post.permalink.replace(post_id, '') - - if(type !== kind && kind) - continue - - if(post.over_18) - if((config.nsfw_enabled === false && user_preferences.nsfw_enabled != 'true') || user_preferences.nsfw_enabled === 'false') - continue - - if(type === 't3') { - let duration = null - if(post.media) { - if(post.is_video) { - if(post.media.reddit_video) { - duration = post.media.reddit_video.duration - } - } - } - - obj = { - type: type, - subreddit: post.subreddit, - title: post.title, - created: post.created_utc, - ups: post.ups, - url: replaceDomains(url, user_preferences), - thumbnail: await downloadAndSave(post.thumbnail), - duration: duration, - edited: post.edited, - selftext_html: unescape(post.selftext_html), - num_comments: post.num_comments, - over_18: post.over_18, - permalink: post.permalink, - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(post) : '') - } - } - if(type === 't1') { - obj = { - type: type, - subreddit: post.subreddit, - title: post.title, - created: post.created_utc, - subreddit_name_prefixed: post.subreddit_name_prefixed, - ups: post.ups, - url: replaceDomains(url, user_preferences), - edited: post.edited, - body_html: unescape(post.body_html), - num_comments: post.num_comments, - over_18: post.over_18, - permalink: post.permalink, - link_author: post.link_author, - link_title: post.link_title, - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(post) : '') - } - } - posts.push(obj) - } - - let obj = { - username: about.name, - icon_img: await downloadAndSave(about.icon_img, "icon_"), - created: about.created_utc, - verified: about.verified, - link_karma: about.link_karma, - comment_karma: about.comment_karma, - view_more_posts: view_more_posts, - user_front: user_front, - post_type: post_type, - before: before, - after: after, - posts: posts - } - - resolve(obj) - })() - }) +const config = require('../config'); +const link = require('./components/link'); + +async function processJsonUser( + json, + parsed, + after, + before, + user_preferences, + kind, + post_type +) { + if (!parsed) { + json = JSON.parse(json); + } + + function validateJson(json) { + const empty = { + username: '', + icon_img: '', + created: '', + verified: '', + link_karma: '', + comment_karma: '', + view_more_posts: '', + user_front: '', + post_type:'', + before: '', + after: '', + posts: [], + }; + + if (!json.overview) { + return { error: true, data: empty }; + } + + if (!json.overview.data) { + return { error: true, data: empty }; + } + + return true; + } + + const validJson = validateJson(json); + if (validJson.error) { + return validJson.data + } + + let about = json.about.data; + let posts = []; + let view_more_posts = false; + let posts_limit = 25; + let user_front = false; + + if (json.overview.data.children.length > posts_limit) { + view_more_posts = true; + } else { + posts_limit = json.overview.data.children.length; + } + + if (!after && !before) { + user_front = true; + } + + if (json.overview.data.children) { + if (json.overview.data.children[posts_limit - 1]) { + after = json.overview.data.children[posts_limit - 1].data.name; + } + if (json.overview.data.children[0]) { + before = json.overview.data.children[0].data.name; + } + } + + for (var i = 0; i < posts_limit; i++) { + let post = json.overview.data.children[i].data; + let thumbnail = 'self'; + let type = json.overview.data.children[i].kind; + let obj; + + let post_id = post.permalink.split('/').slice(-2)[0] + '/'; + let url = post.permalink.replace(post_id, ''); + + if (type !== kind && kind) continue; + + if (post.over_18) + if ( + (config.nsfw_enabled === false && + user_preferences.nsfw_enabled != 'true') || + user_preferences.nsfw_enabled === 'false' + ) + continue; + + if (type === 't3') { + obj = await link.fromJson(post, user_preferences); + obj.type = 't3'; + } + if (type === 't1') { + obj = { + type: type, + subreddit: post.subreddit, + title: post.title, + created: post.created_utc, + subreddit_name_prefixed: post.subreddit_name_prefixed, + ups: post.ups, + url: replaceDomains(url, user_preferences), + edited: post.edited, + body_html: unescape(post.body_html), + num_comments: post.num_comments, + over_18: post.over_18, + permalink: post.permalink, + link_author: post.link_author, + link_title: post.link_title, + user_flair: + user_preferences.flairs != 'false' ? await formatUserFlair(post) : '', + }; + } + posts.push(obj); } + + let obj = { + username: about.name, + icon_img: await downloadAndSave(about.icon_img, 'icon_'), + created: about.created_utc, + verified: about.verified, + link_karma: about.link_karma, + comment_karma: about.comment_karma, + view_more_posts: view_more_posts, + user_front: user_front, + post_type: post_type, + before: before, + after: after, + posts: posts, + }; + + return obj; } + +module.exports = processJsonUser; diff --git a/inc/processMoreComments.js b/inc/processMoreComments.js index f8243d54..bea346cf 100644 --- a/inc/processMoreComments.js +++ b/inc/processMoreComments.js @@ -1,54 +1,51 @@ -module.exports = function() { - const config = require('../config') - this.moreComments = (fetch, redis, post_url, comment_ids, id) => { - return new Promise(resolve => { - (async () => { - let key = `${post_url}:morechildren:comment_ids:${comment_ids}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the ${key} key from redis (moreComments()).`, error) - resolve(false) +const config = require('../config'); +const { redisAsync } = require('./redis'); + +async function processMoreComments(fetch, redis, post_url, comment_ids, id) { + if (post_url) { + let key = `${post_url}:morechildren:comment_ids:${comment_ids}` + + try { + const cached = await redisAsync.get(key); + + if (cached !== null) { + return JSON.parse(cached); + } + let url = `https://oauth.reddit.com/api/morechildren?api_type=json&children=${comment_ids}&limit_children=false&link_id=t3_${id}` + const moreCommentsRequest = await fetch(url, redditApiGETHeaders()); + + if (moreCommentsRequest.ok) { + let response = await moreCommentsRequest.json(); + + if (response.json.data) { + if (response.json.data.things) { + let comments = response.json.data.things + await redisAsync.setex( + key, + config.setexs.posts, + JSON.stringify(comments) + ); + console.log(`Fetched more comments.`); + + return comments; } - if(json) { - json = JSON.parse(json) - resolve(json) - } else { - let url = `https://oauth.reddit.com/api/morechildren?api_type=json&children=${comment_ids}&limit_children=false&link_id=t3_${id}` - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - if(json.json.data) { - if(json.json.data.things) { - let comments = json.json.data.things - redis.setex(key, config.setexs.posts, JSON.stringify(comments), (error) => { - if(error) { - console.error(`Error setting the ${key} key to redis (moreComments()).`, error) - resolve(false) - } else { - console.log(`Fetched the JSON from Reddit (endpoint "morechildren") for URL: ${post_url}. (moreComments())`) - resolve(comments) - } - }) - } else { - resolve(false) - } - } else { - resolve(false) - } - }) - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText} (moreComments())`) - resolve(false) - } - }).catch(error => { - console.log(`Error fetching the JSON from Reddit (endpoint "morechildren") with url: ${url}. (moreComments())`, error) - resolve(false) - }) - } - }) - })() - }) + } + } else { + console.error( + `Something went wrong while fetching data from Reddit: + ${moreCommentsRequest.status} – ${moreCommentsRequest.statusText}` + ); + console.error(config.reddit_api_error_text); + return null; + } + } catch (error) { + console.error('Error fetching more comments: ', error); + + return null; + } + } else { + return null; } } + +module.exports = processMoreComments; diff --git a/inc/processPostMedia.js b/inc/processPostMedia.js index da1b83a9..1834b2fb 100644 --- a/inc/processPostMedia.js +++ b/inc/processPostMedia.js @@ -92,12 +92,23 @@ module.exports = function() { obj.has_media = true if(!gif_to_mp4) { if(post.preview) { - obj.media = { - source: await downloadAndSave(post.preview.reddit_video_preview.fallback_url), - height: post.preview.reddit_video_preview.height, - width: post.preview.reddit_video_preview.width, - duration: post.preview.reddit_video_preview.duration, - is_gif: true + if(post.preview.reddit_video_preview) { + const url = post.domain === 'i.imgur.com' + ? replaceDomains(post.url_overridden_by_dest.replace(/\.gifv$/, '.mp4')) + : post.preview.reddit_video_preview.fallback_url; + if(url) { + obj.media = { + source: await downloadAndSave(url), + height: post.preview.reddit_video_preview.height, + width: post.preview.reddit_video_preview.width, + duration: post.preview.reddit_video_preview.duration, + is_gif: true + } + } else { + obj.has_media = false + } + } else { + obj.has_media = false } } else { obj.has_media = false @@ -119,12 +130,26 @@ module.exports = function() { */ if(!post_media && !has_gif && !post.gallery_data && post.url != '') { try { - let u = new URL(post.url) + let url = replaceDomains(post.url) + const u = new URL(url) if(config.valid_media_domains.includes(u.hostname)) { - let ext = u.pathname.split('.')[1] - if(ext === 'jpg' || ext === 'png') { + const ext = u.pathname.split('.')[1] + if(['jpg', 'png', 'jpeg', 'gif'].includes(ext)) { obj.images = { - source: await downloadAndSave(post.url) + source: await downloadAndSave(url) + } + } + else if(['gifv', 'mp4'].includes(ext)) { + if (obj.domain === 'i.imgur.com') { + url = url.replace(/\.gifv$/, '.mp4'); + } + obj.has_media = true + obj.media = { + source: await downloadAndSave(url) + } + if (post.preview && post.preview.images) { + obj.media.height = post.preview.images[0].source.height; + obj.media.width = post.preview.images[0].source.width; } } } diff --git a/inc/processSearchResults.js b/inc/processSearchResults.js index bb71ef23..c968d595 100644 --- a/inc/processSearchResults.js +++ b/inc/processSearchResults.js @@ -1,110 +1,70 @@ -module.exports = function() { - const config = require('../config'); - this.processSearchResults = (json, parsed, after, before, user_preferences) => { - return new Promise(resolve => { - (async () => { - if(!parsed) { - json = JSON.parse(json) - } - let posts = [] - let search_firstpage = false - let before = json.data.before - let after = json.data.after - - if(!after && !before) { - search_firstpage = true - } - - let suggested_subreddits = false - if(json.suggested_subreddits) { - if(json.suggested_subreddits.data) { - if(json.suggested_subreddits.data.children.length > 0) { - suggested_subreddits = json.suggested_subreddits.data.children - } - } - } - - if(json.data.children) { - let view_more_posts = false - let posts_limit = 25 +const config = require('../config'); +const link = require('./components/link'); - if(json.data.children.length > posts_limit) { - view_more_posts = true - } else { - posts_limit = json.data.children.length - } - - for(var i = 0; i < posts_limit; i++) { - let post = json.data.children[i].data - let images = null - let is_self_link = false - let valid_reddit_self_domains = ['reddit.com'] - - if(post.over_18) - if((config.nsfw_enabled === false && user_preferences.nsfw_enabled != 'true') || user_preferences.nsfw_enabled === 'false') - continue +async function processSearchResults( + json, + parsed, + after, + before, + user_preferences +) { + if (!parsed) { + json = JSON.parse(json); + } + let posts = []; + let search_firstpage = false; - if(post.domain) { - let tld = post.domain.split('self.') - if(tld.length > 1) { - if(!tld[1].includes('.')) { - is_self_link = true - } - } - if(config.valid_media_domains.includes(post.domain) || valid_reddit_self_domains.includes(post.domain)) { - is_self_link = true - } - } + before = json.data.before; + after = json.data.after; - if(post.preview && post.thumbnail !== 'self') { - if(!post.url.startsWith('/r/') && isGif(post.url)) { - images = { - thumb: await downloadAndSave(post.thumbnail, 'thumb_') - } - } else { - if(post.preview.images[0].resolutions[0]) { - images = { - thumb: await downloadAndSave(post.preview.images[0].resolutions[0].url, 'thumb_') - } - } - } - } - - let obj = { - subreddit: post.subreddit, - title: post.title, - created: post.created_utc, - domain: post.domain, - subreddit_name_prefixed: post.subreddit_name_prefixed, - link_flair_text: post.link_flair_text, - ups: post.ups, - images: images, - url: replaceDomains(post.url, user_preferences), - edited: post.edited, - selftext_html: unescape(post.body_html), - num_comments: post.num_comments, - over_18: post.over_18, - permalink: post.permalink, - is_self_link: is_self_link, - author: post.author, - link_title: post.link_title, - link_flair: (user_preferences.flairs != 'false' ? await formatLinkFlair(post) : ''), - user_flair: (user_preferences.flairs != 'false' ? await formatUserFlair(post) : '') - } - posts.push(obj) - } - } - - let obj = { - search_firstpage: search_firstpage, - before: before, - after: after, - posts: posts, - suggested_subreddits: suggested_subreddits, - } + if (!after && !before) { + search_firstpage = true; + } - resolve(obj) - })() - }) + let suggested_subreddits = false; + if (json.suggested_subreddits) { + if (json.suggested_subreddits.data) { + if (json.suggested_subreddits.data.children.length > 0) { + suggested_subreddits = json.suggested_subreddits.data.children; + } + } } + + if (json.data.children) { + let view_more_posts = false; + let posts_limit = 25; + + if (json.data.children.length > posts_limit) { + view_more_posts = true; + } else { + posts_limit = json.data.children.length; + } + + for (var i = 0; i < posts_limit; i++) { + let post = json.data.children[i].data; + + if (post.over_18) + if ( + (config.nsfw_enabled === false && + user_preferences.nsfw_enabled != 'true') || + user_preferences.nsfw_enabled === 'false' + ) + continue; + + let obj = await link.fromJson(post, user_preferences); + posts.push(obj); + } + } + + let obj = { + search_firstpage: search_firstpage, + before: before, + after: after, + posts: posts, + suggested_subreddits: suggested_subreddits, + }; + + return obj; } + +module.exports = processSearchResults; diff --git a/inc/processSubredditAbout.js b/inc/processSubredditAbout.js index aeadf110..2d7ad052 100644 --- a/inc/processSubredditAbout.js +++ b/inc/processSubredditAbout.js @@ -1,95 +1,78 @@ -module.exports = function() { - const config = require('../config') - this.processSubredditAbout = (subreddit, redis, fetch, RedditAPI) => { - return new Promise(resolve => { - (async () => { - if(subreddit && !subreddit.includes('+') && subreddit !== 'all') { - function returnRelevantKeys(json) { - return { - title: json.data.title, - public_description_html: json.data.public_description_html, - active_user_count: json.data.active_user_count, - subscribers: json.data.subscribers, - created_utc: json.data.created_utc, - over18: json.data.over18, - description_html: json.data.description_html, - moderators: json.moderators - } - } - - let key = `${subreddit}:sidebar` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the ${subreddit}:sidebar key from redis.`, error) - resolve(null) - } - if(json) { - json = JSON.parse(json) - resolve(returnRelevantKeys(json)) - } else { - let url = `https://reddit.com/r/${subreddit}/about.json` - if(config.use_reddit_oauth) { - url = `https://oauth.reddit.com/r/${subreddit}/about` - } - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - json.moderators = [] - redis.setex(key, config.setexs.sidebar, JSON.stringify(json), (error) => { - if(error) { - console.error('Error setting the sidebar key to redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } else { - console.log('Fetched the sidebar from reddit API.') - let moderators_url = `https://reddit.com/r/${subreddit}/about/moderators.json` - if(config.use_reddit_oauth) { - moderators_url = `https://oauth.reddit.com/r/${subreddit}/about/moderators` - } - fetch(encodeURI(moderators_url), redditApiGETHeaders()) - .then(mod_result => { - if(mod_result.status === 200) { - mod_result.json() - .then(mod_json => { - json.moderators = mod_json - redis.setex(key, config.setexs.sidebar, JSON.stringify(json), (error) => { - if(error) { - console.error('Error setting the sidebar with moderators key to redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } else { - console.log('Fetched the moderators from reddit API.') - resolve(returnRelevantKeys(json)) - } - }) - }) - } else { - console.error(`Something went wrong while fetching moderators data from reddit API. ${mod_result.status} – ${mod_result.statusText}`) - console.error(config.reddit_api_error_text) - resolve(returnRelevantKeys(json)) - } - }).catch(error => { - console.error('Error fetching moderators.', error) - resolve(returnRelevantKeys(json)) - }) - } - }) - }) - } else { - console.error(`Something went wrong while fetching data from reddit API. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - resolve(null) - } - }).catch(error => { - console.error('Error fetching the sidebar.', error) - resolve(null) - }) - } - }) - } else { - resolve(null) - } - })() - }) +const config = require('../config'); +const { redisAsync } = require('./redis'); + +function returnRelevantKeys(json) { + return { + title: json.data.title, + public_description_html: json.data.public_description_html, + active_user_count: json.data.active_user_count, + subscribers: json.data.subscribers, + created_utc: json.data.created_utc, + over18: json.data.over18, + description_html: json.data.description_html, + moderators: json.moderators, + }; +} + +async function processSubredditAbout(subreddit, redis, fetch, RedditAPI) { + if (subreddit && !subreddit.includes('+') && subreddit !== 'all') { + const key = `${subreddit}:sidebar`; + + try { + const cached = await redisAsync.get(key); + + if (cached !== null) { + return returnRelevantKeys(JSON.parse(cached)); + } + + let url = `https://reddit.com/r/${subreddit}/about.json`; + + if (config.use_reddit_oauth) { + url = `https://oauth.reddit.com/r/${subreddit}/about`; + } + + const subredditAboutRequest = await fetch(url, redditApiGETHeaders()); + + if (subredditAboutRequest.ok) { + let response = await subredditAboutRequest.json(); + response.moderators = []; + + await redisAsync.setex( + key, + config.setexs.sidebar, + JSON.stringify(response) + ); + + console.log(`Fetched sidebar for ${subreddit} from reddit API`); + + return returnRelevantKeys(response); + } else { + console.error( + `Something went wrong while fetching data from reddit API: + ${subredditAboutRequest.status} – ${subredditAboutRequest.statusText}` + ); + console.error(config.reddit_api_error_text); + return null; + } + } catch (error) { + console.error('Error fetching the sidebar: ', error); + + return null; + } + } else { + return null; + } +} + +async function processJsonSubredditAbout(json, parsed) { + if (!parsed) { + json = JSON.parse(json); } + + return returnRelevantKeys(json); } + +module.exports = { + processSubredditAbout, + processJsonSubredditAbout +}; diff --git a/inc/processSubredditsExplore.js b/inc/processSubredditsExplore.js index ab3f931c..66ed2f96 100644 --- a/inc/processSubredditsExplore.js +++ b/inc/processSubredditsExplore.js @@ -1,52 +1,58 @@ -module.exports = function() { - const config = require('../config'); - this.processJsonSubredditsExplore = (json, from, subreddit_front, user_preferences) => { - return new Promise(resolve => { - (async () => { - if(from === 'redis') { - json = JSON.parse(json) - } - if(json.error) { - resolve({ error: true, error_data: json }) - } else { - let before = json.data.before - let after = json.data.after +const config = require('../config'); - let ret = { - info: { - before: before, - after: after - }, - links: [] - } +async function processJsonSubredditsExplore( + json, + from, + subreddit_front, + user_preferences +) { + if (from === 'redis') { + json = JSON.parse(json); + } + if (json.error) { + return { error: true, error_data: json }; + } else { + let before = json.data.before; + let after = json.data.after; + + let ret = { + info: { + before: before, + after: after, + }, + links: [], + }; + + let children_len = json.data.children.length; - let children_len = json.data.children.length + for (var i = 0; i < children_len; i++) { + let data = json.data.children[i].data; - for(var i = 0; i < children_len; i++) { - let data = json.data.children[i].data - - if(data.over_18) - if((config.nsfw_enabled === false && user_preferences.nsfw_enabled != 'true') || user_preferences.nsfw_enabled === 'false') - continue - - let obj = { - created: data.created_utc, - id: data.id, - over_18: data.over_18, - display_name: data.display_name, - display_name_prefixed: data.display_name_prefixed, - public_description: data.public_description, - url: replaceDomains(data.url, user_preferences), - subscribers: data.subscribers, - over_18: data.over18, - title: data.title, - subreddit_front: subreddit_front, - } - ret.links.push(obj) - } - resolve(ret) - } - })() - }) + if (data.over_18) + if ( + (config.nsfw_enabled === false && + user_preferences.nsfw_enabled != 'true') || + user_preferences.nsfw_enabled === 'false' + ) + continue; + + let obj = { + created: data.created_utc, + id: data.id, + over_18: data.over_18, + display_name: data.display_name, + display_name_prefixed: data.display_name_prefixed, + public_description: data.public_description, + url: replaceDomains(data.url, user_preferences), + subscribers: data.subscribers, + over_18: data.over18, + title: data.title, + subreddit_front: subreddit_front, + }; + ret.links.push(obj); + } + return ret; } } + +module.exports = processJsonSubredditsExplore; diff --git a/inc/redis.js b/inc/redis.js new file mode 100644 index 00000000..0918d9d2 --- /dev/null +++ b/inc/redis.js @@ -0,0 +1,51 @@ +const config = require('../config'); +const { promisify } = require('util'); +const r = require('redis'); + +const redisOptions = { + host: '127.0.0.1', + port: 6379, +}; + +if (config.redis_db) { + redisOptions.db = config.redis_db; +} + +if (config.redis_host) { + redisOptions.host = config.redis_host; +} + +if (config.redis_port && config.redis_port > 0) { + redisOptions.port = config.redis_port; +} + +if (config.redis_password) { + redisOptions.password = config.redis_password; +} + +// Stub Redis if disabled +const stub = { + get: (_, callback) => callback(null, null), + setex: (_, _1, _2, callback) => callback(null), + on: () => {}, +}; + +const redisDisabled = !config.redis_enabled; + +const redis = redisDisabled ? stub : r.createClient(redisOptions); + +const redisAsync = { + get: promisify(redis.get).bind(redis), + setex: promisify(redis.setex).bind(redis), +}; + +redis.on('error', (error) => { + if (error) { + console.error(`Redis error: ${error}`); + } +}); + +module.exports = { + redis, + redisAsync, +}; diff --git a/inc/teddit_api/handlePost.js b/inc/teddit_api/handlePost.js new file mode 100644 index 00000000..b5877e49 --- /dev/null +++ b/inc/teddit_api/handlePost.js @@ -0,0 +1,87 @@ +const { processJsonPost, getPostItem } = require('../processJsonPost'); + +module.exports = function () { + const config = require('../../config'); + this.handleTedditApiPost = async ( + json, + req, + res, + from, + api_type, + api_target + ) => { + if (!config.api_enabled) { + res.setHeader('Content-Type', 'application/json'); + let msg = { + info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.', + }; + return res.end(JSON.stringify(msg)); + } + + console.log('Teddit API request - post'); + if (from === 'redis') json = JSON.parse(json); + + if (api_type === 'rss') { + let protocol = config.https_enabled || config.api_force_https ? 'https' : 'http'; + let items = ''; + + let post = json[0].data.children[0].data; + let comments = json[1].data.children; + + items += await getPostItem(post, req, protocol); + + for (var i = 0; i < comments.length; i++) { + let comment = comments[i].data; + let kind = comments[i].kind; + + let title = `/u/${comment.author} on ${post.title}`; + + comment.permalink = `${protocol}://${config.domain}${comment.permalink}`; + + if (kind !== 'more') { + items += ` + + ${title} + ${comment.author} + ${comment.created} + ${new Date( + comment.created_utc * 1000 + ).toGMTString()} + ${comment.id} + ${comment.permalink} + + ${comment.ups} + + `; + } + } + + let xml_output = ` + + + + ${post.title} + ${post.url} + ${items} + + `; + res.setHeader('Content-Type', 'application/rss+xml'); + return res.end(xml_output); + } else { + res.setHeader('Content-Type', 'application/json'); + if (api_target === 'reddit') { + return res.end(JSON.stringify(json)); + } else { + let processed_json = await processJsonPost( + json, + true, + req.cookies + ); + + return res.end(JSON.stringify(processed_json)); + } + } + }; +}; diff --git a/inc/teddit_api/handleSubreddit.js b/inc/teddit_api/handleSubreddit.js index f1359403..9b063c32 100644 --- a/inc/teddit_api/handleSubreddit.js +++ b/inc/teddit_api/handleSubreddit.js @@ -1,114 +1,52 @@ -module.exports = function() { - const config = require('../../config') - this.handleTedditApiSubreddit = async (json, req, res, from, api_type, api_target, subreddit) => { - if(!config.api_enabled) { - res.setHeader('Content-Type', 'application/json') - let msg = { info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.' } - return res.end(JSON.stringify(msg)) +const processJsonSubreddit = require('../processJsonSubreddit'); +const { processJsonSubredditAbout } = require('../processSubredditAbout'); +const processSearchResults = require('../processSearchResults.js'); +const { processJsonPostList, getPostItem } = require('../processJsonPost'); +const processJsonSubredditsExplore = require('../processSubredditsExplore.js'); + +module.exports = function () { + const config = require('../../config'); + this.handleTedditApiSubreddit = async ( + json, + req, + res, + from, + api_type, + api_target, + subreddit, + mode + ) => { + if (!config.api_enabled) { + res.setHeader('Content-Type', 'application/json'); + let msg = { + info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.', + }; + return res.end(JSON.stringify(msg)); } - - console.log('Teddit API request - subreddit') - let _json = json // Keep the original json - if(from === 'redis') - json = JSON.parse(json) - - if(api_type === 'rss') { - let protocol = (config.https_enabled ? 'https' : 'http') - let items = '' - for(var i = 0; i < json.data.children.length; i++) { - let link = json.data.children[i].data - let thumbnail = '' - let post_image = '' - let is_self_link = false - let valid_reddit_self_domains = ['reddit.com'] - - if(link.domain) { - let tld = link.domain.split('self.') - if(tld.length > 1) { - if(!tld[1].includes('.')) { - is_self_link = true - link.url = teddifyUrl(link.url) - } - } - if(config.valid_media_domains.includes(link.domain) || valid_reddit_self_domains.includes(link.domain)) { - is_self_link = true - link.url = teddifyUrl(link.url) - } - } - - if(link.preview && link.thumbnail !== 'self') { - if(!link.url.startsWith('/r/') && isGif(link.url)) { - let s = await downloadAndSave(link.thumbnail, 'thumb_') - thumbnail = `${protocol}://${config.domain}${s}` - } else { - if(link.preview.images[0].resolutions[0]) { - let s = await downloadAndSave(link.preview.images[0].resolutions[0].url, 'thumb_') - thumbnail = `${protocol}://${config.domain}${s}` - if(!isGif(link.url) && !link.post_hint.includes(':video')) { - s = await downloadAndSave(link.preview.images[0].source.url) - post_image = `${protocol}://${config.domain}${s}` - } - } - } - } - - link.permalink = `${protocol}://${config.domain}${link.permalink}` - - if(is_self_link) - link.url = link.permalink - - if(req.query.hasOwnProperty('full_thumbs')) { - if(!post_image) - post_image = thumbnail - - thumbnail = post_image - } - - let enclosure = '' - if(thumbnail != '') { - let mime = '' - let ext = thumbnail.split('.').pop() - if(ext === 'png') - mime = 'image/png' - else - mime = 'image/jpeg' - enclosure = `` - } - - let append_desc_html = `
[link] [comments]` - - items += ` - - ${link.title} - ${link.author} - ${link.created} - ${new Date(link.created_utc*1000).toGMTString()} - ${link.domain} - ${link.id} - ${thumbnail} - ${enclosure} - ${link.permalink} - ${link.url} - - ${link.num_comments} - ${link.ups} - ${link.stickied} - ${is_self_link} - - ` + + console.log('Teddit API request - subreddit'); + let _json = json; // Keep the original json + if (from === 'redis') json = JSON.parse(json); + + if (api_type === 'rss') { + let protocol = config.https_enabled || config.api_force_https ? 'https' : 'http'; + let items = ''; + + for (var i = 0; i < json.data.children.length; i++) { + let post = json.data.children[i].data; + items += await getPostItem(post, req, protocol); } - - let r_subreddit = '/r/' + subreddit - let title = r_subreddit - let link = `${protocol}://${config.domain}${r_subreddit}` - if(subreddit === '/') { - r_subreddit = 'frontpage' - title = 'teddit frontpage' - link = `${protocol}://${config.domain}` + + let r_subreddit = '/r/' + subreddit; + let title = r_subreddit; + let link = `${protocol}://${config.domain}${r_subreddit}`; + if (subreddit === '/') { + r_subreddit = 'frontpage'; + title = 'teddit frontpage'; + link = `${protocol}://${config.domain}`; } - - let xml_output = - ` + + let xml_output = ` @@ -117,50 +55,203 @@ module.exports = function() { Subreddit feed for: ${r_subreddit} ${items} - ` - res.setHeader('Content-Type', 'application/rss+xml') - return res.end(xml_output) + `; + res.setHeader('Content-Type', 'application/rss+xml'); + return res.end(xml_output); } else { - res.setHeader('Content-Type', 'application/json') - if(api_target === 'reddit') { - return res.end(JSON.stringify(json)) + res.setHeader('Content-Type', 'application/json'); + if (api_target === 'reddit') { + return res.end(JSON.stringify(json)); } else { - let processed_json = await processJsonSubreddit(_json, from, null, req.cookies) - - let protocol = (config.https_enabled ? 'https' : 'http') - for(var i = 0; i < processed_json.links.length; i++) { - let link = processed_json.links[i] - let valid_reddit_self_domains = ['reddit.com'] - let is_self_link = false - - if(link.domain) { - let tld = link.domain.split('self.') - if(tld.length > 1) { - if(!tld[1].includes('.')) { - is_self_link = true - link.url = teddifyUrl(link.url) - } - } - if(config.valid_media_domains.includes(link.domain) || valid_reddit_self_domains.includes(link.domain)) { - is_self_link = true - link.url = teddifyUrl(link.url) - } - } - - link.permalink = `${protocol}://${config.domain}${link.permalink}` - - if(is_self_link) - link.url = link.permalink - - if(link.images) { - if(link.images.thumb !== 'self') { - link.images.thumb = `${protocol}://${config.domain}${link.images.thumb}` - } - } - } + let processed_json = await processJsonSubreddit( + _json, + from, + null, + req.cookies + ); + + await processJsonPostList(processed_json.links, mode); + + return res.end(JSON.stringify(processed_json)); + } + } + }; + this.handleTedditApiSubredditAbout = async ( + json, + res, + from, + api_target + ) => { + if (!config.api_enabled) { + res.setHeader('Content-Type', 'application/json'); + let msg = { + info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.', + }; + return res.end(JSON.stringify(msg)); + } + + console.log('Teddit API request - subreddit about'); + if (from === 'redis') json = JSON.parse(json); + + res.setHeader('Content-Type', 'application/json'); + + if (api_target === 'reddit') { + return res.end(JSON.stringify(json)); + } else { + let subreddit_about = await processJsonSubredditAbout( + json, + true + ); + + return res.end(JSON.stringify(subreddit_about)); + } + }; + this.handleTedditApiSubredditSearch = async ( + json, + req, + res, + from, + api_type, + api_target, + subreddit, + query, + mode + ) => { + if (!config.api_enabled) { + res.setHeader('Content-Type', 'application/json'); + let msg = { + info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.', + }; + return res.end(JSON.stringify(msg)); + } + + console.log('Teddit API request - subreddit search'); + if (from === 'redis') json = JSON.parse(json); + + if (api_type === 'rss') { + let protocol = config.https_enabled || config.api_force_https ? 'https' : 'http'; + let items = ''; + + for (var i = 0; i < json.data.children.length; i++) { + let post = json.data.children[i].data; + items += await getPostItem(post, req, protocol); + } + + let r_subreddit = '/r/' + subreddit; + let title = `${query} - ${r_subreddit}`; + let link = `${protocol}://${config.domain}${r_subreddit}/search?q=${query}`; + + let xml_output = ` + + + + ${title} + ${link} + Results for: ${query} - ${r_subreddit} + ${items} + + `; + + res.setHeader('Content-Type', 'application/rss+xml'); + + return res.end(xml_output); + } else { + res.setHeader('Content-Type', 'application/json'); + if (api_target === 'reddit') { + return res.end(JSON.stringify(json)); + } else { + let processed_json = await processSearchResults( + json, + true, + null, + null, + req.cookies + ); + + await processJsonPostList(processed_json.posts, mode); + + return res.end(JSON.stringify(processed_json)); + } + } + }; + this.handleTedditApiSubredditsExplore = async ( + json, + req, + res, + from, + api_type, + api_target, + query + ) => { + if (!config.api_enabled) { + res.setHeader('Content-Type', 'application/json'); + let msg = { + info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.', + }; + return res.end(JSON.stringify(msg)); + } + + console.log('Teddit API request - subreddit explore'); + if (from === 'redis') json = JSON.parse(json); + + if (api_type === 'rss') { + let protocol = config.https_enabled || config.api_force_https ? 'https' : 'http'; + let items = ''; + + let children_len = json.data.children.length; + + for (var i = 0; i < children_len; i++) { + let data = json.data.children[i].data; + + items += ` + + ${data.title} + ${data.created} + ${new Date( + data.created_utc * 1000 + ).toGMTString()} + ${data.id} + ${data.display_name} + ${data.display_name_prefixed} + ${replaceDomains(data.url, req.cookies)} + + ${data.subscribers} + ${data.over18} + + `; + } - return res.end(JSON.stringify(processed_json)) - } + let title = `${query}`; + let link = `${protocol}://${config.domain}/subreddits/search?q=${query}`; + + let xml_output = ` + + + + ${title} + ${link} + Results for: ${query} + ${items} + + `; + res.setHeader('Content-Type', 'application/rss+xml'); + return res.end(xml_output); + } else { + res.setHeader('Content-Type', 'application/json'); + if (api_target === 'reddit') { + return res.end(JSON.stringify(json)); + } else { + let processed_json = await processJsonSubredditsExplore( + json, + true, + null, + req.cookies + ); + + return res.end(JSON.stringify(processed_json)); + } } - } -} + }; +}; diff --git a/inc/teddit_api/handleUser.js b/inc/teddit_api/handleUser.js index 3efde609..1d862218 100644 --- a/inc/teddit_api/handleUser.js +++ b/inc/teddit_api/handleUser.js @@ -1,74 +1,86 @@ -module.exports = function() { - const config = require('../../config') - this.handleTedditApiUser = async (json, req, res, from, api_type, api_target, user) => { - if(!config.api_enabled) { - res.setHeader('Content-Type', 'application/json') - let msg = { info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.' } - return res.end(JSON.stringify(msg)) +const processJsonUser = require('../processJsonUser'); + +module.exports = function () { + const config = require('../../config'); + this.handleTedditApiUser = async ( + json, + req, + res, + from, + api_type, + api_target, + user + ) => { + if (!config.api_enabled) { + res.setHeader('Content-Type', 'application/json'); + let msg = { + info: 'This instance do not support API requests. Please see https://codeberg.org/teddit/teddit#instances for instances that support API, or setup your own instance.', + }; + return res.end(JSON.stringify(msg)); } - - console.log('Teddit API request - user') - let _json = json // Keep the original json - if(from === 'redis') - json = JSON.parse(json) - - let protocol = (config.https_enabled ? 'https' : 'http') - let link = `${protocol}://${config.domain}/user/${user}` - - if(api_type === 'rss') { - let items = '' - let posts_limit = 25 - - if(json.overview.data.children.length <= posts_limit) { - posts_limit = json.overview.data.children.length + + console.log('Teddit API request - user'); + let _json = json; // Keep the original json + if (from === 'redis') json = JSON.parse(json); + + let protocol = config.https_enabled || config.api_force_https ? 'https' : 'http'; + let link = `${protocol}://${config.domain}/user/${user}`; + + if (api_type === 'rss') { + let items = ''; + let posts_limit = 25; + + if (json.overview.data.children.length <= posts_limit) { + posts_limit = json.overview.data.children.length; } - - for(var i = 0; i < posts_limit; i++) { - let post = json.overview.data.children[i].data - let post_id = post.permalink.split('/').slice(-2)[0] + '/' - let url = post.permalink.replace(post_id, '') - let permalink = `${protocol}://${config.domain}${post.permalink}` - let comments_url = `${protocol}://${config.domain}${url}` - let kind = json.overview.data.children[i].kind - - let t1_elements = '' - let t3_elements = '' - if(kind === 't1') { - let append_desc_html = `
[link] [comments]` + + for (var i = 0; i < posts_limit; i++) { + let post = json.overview.data.children[i].data; + let post_id = post.permalink.split('/').slice(-2)[0] + '/'; + let url = post.permalink.replace(post_id, ''); + let permalink = `${protocol}://${config.domain}${post.permalink}`; + let comments_url = `${protocol}://${config.domain}${url}`; + let kind = json.overview.data.children[i].kind; + + let t1_elements = ''; + let t3_elements = ''; + if (kind === 't1') { + let append_desc_html = `
[link] [comments]`; t1_elements = ` - + ${comments_url} - ` + `; } - if(kind === 't3') { - let s = await downloadAndSave(post.thumbnail, 'thumb_') - let thumbnail = '' - let enclosure = '' - if(s !== 'self' && s != '') { - let img = `${protocol}://${config.domain}${s}` - thumbnail = `${img}` - - let mime = '' - let ext = s.split('.').pop() - if(ext === 'png') - mime = 'image/png' - else - mime = 'image/jpeg' - enclosure = `` + if (kind === 't3') { + let s = await downloadAndSave(post.thumbnail, 'thumb_'); + let thumbnail = ''; + let enclosure = ''; + if (s !== 'self' && s != '') { + let img = `${protocol}://${config.domain}${s}`; + thumbnail = `${img}`; + + let mime = ''; + let ext = s.split('.').pop(); + if (ext === 'png') mime = 'image/png'; + else mime = 'image/jpeg'; + enclosure = ``; } - let append_desc_html = `submitted by r/${post.subreddit}` - append_desc_html += `
[comments]` + let append_desc_html = `submitted by r/${post.subreddit}`; + append_desc_html += `
[comments]`; t3_elements = ` - + ${thumbnail} ${enclosure} - ` + `; } - - let title = post.title - if(!post.title) - title = post.link_title - + + let title = post.title; + if (!post.title) title = post.link_title; + items += ` ${title} @@ -76,7 +88,9 @@ module.exports = function() { ${kind} ${post.subreddit} ${post.created_utc} - ${new Date(post.created_utc*1000).toGMTString()} + ${new Date( + post.created_utc * 1000 + ).toGMTString()} ${post.ups} ${permalink} ${post.edited} @@ -85,11 +99,10 @@ module.exports = function() { ${t1_elements} ${t3_elements} - ` + `; } - - let xml_output = - ` + + let xml_output = ` @@ -97,17 +110,23 @@ module.exports = function() { ${link} ${items} - ` - res.setHeader('Content-Type', 'application/rss+xml') - return res.end(xml_output) + `; + res.setHeader('Content-Type', 'application/rss+xml'); + return res.end(xml_output); } else { - res.setHeader('Content-Type', 'application/json') - if(api_target === 'reddit') { - return res.end(JSON.stringify(json)) + res.setHeader('Content-Type', 'application/json'); + if (api_target === 'reddit') { + return res.end(JSON.stringify(json)); } else { - let processed_json = await processJsonUser(json, true, null, null, req.cookies) - return res.end(JSON.stringify(processed_json)) - } + let processed_json = await processJsonUser( + json, + true, + null, + null, + req.cookies + ); + return res.end(JSON.stringify(processed_json)); + } } - } -} + }; +}; diff --git a/instances.json b/instances.json new file mode 100644 index 00000000..2eb7f172 --- /dev/null +++ b/instances.json @@ -0,0 +1,115 @@ +[ + { + "url": "https://teddit.net" + }, + { + "url": "https://teddit.ggc-project.de" + }, + { + "url": "https://teddit.zaggy.nl" + }, + { + "url": "https://teddit.tinfoil-hat.net" + }, + { + "url": "https://teddit.domain.glass" + }, + { + "url": "https://snoo.ioens.is", + "onion": "http://snoo.ioensistjs7wd746zluwixvojbbkxhr37lepdvwtdfeav673o64iflqd.onion" + }, + { + "url": "https://teddit.httpjames.space" + }, + { + "url": "https://teddit.xbdm.fun" + }, + { + "url": "", + "onion": "http://ibarajztopxnuhabfu7fg6gbudynxofbnmvis3ltj6lfx47b6fhrd5qd.onion", + "i2p": "http://xugoqcf2pftm76vbznx4xuhrzyb5b6zwpizpnw2hysexjdn5l2tq.b32.i2p", + "notes": "Operated by mdleom.com" + }, + { + "url": "https://incogsnoo.com", + "onion": "http://tedditfyn6idalzso5wam5qd3kdtxoljjhbrbbx34q2xkcisvshuytad.onion", + "i2p": "http://teddit.i2p" + }, + { + "url": "https://teddit.pussthecat.org", + "notes": "Operated by PussTheCat.org" + }, + { + "url": "https://reddit.lol", + "onion": "http://dawtyi5e2cfyfmoht4izmczi42aa2zwh6wi34zwvc6rzf2acpxhrcrad.onion", + "i2p": "http://vzeiwzi7ogwl3ijrfek4fbtwhvamxcpyqoc3s4vcgnhlp54s5clq.b32.i2p", + "notes": "Operated by https://liberta.casa" + }, + { + "url": "https://teddit.sethforprivacy.com", + "onion": "http://qtpvyiaqhmwccxwzsqubd23xhmmrt75tdyw35kp43w4hvamsgl3x27ad.onion", + "notes": "For more similar hosted tools, see blog.sethforprivacy.com" + }, + { + "url": "https://teddit.adminforge.de", + "notes": "Operated by https://adminforge.de" + }, + { + "url": "https://teddit.bus-hit.me", + "notes": "Operated by https://bus-hit.me" + }, + { + "url": "https://teddit.froth.zone" + }, + { + "url": "https://rdt.trom.tf/", + "notes": "Part of the https://trom.tf/ project" + }, + { + "url": "https://teddit.encrypted-data.xyz" + }, + { + "url": "https://i.opnxng.com" + }, + { + "url": "https://teddit.tokhmi.xyz" + }, + { + "url": "https://teddit.garudalinux.org", + "notes": "Managed by https://garudalinux.org" + }, + { + "url": "https://teddit.privacytools.io", + "onion": "http://jnuonmf2n36sfdmyksqqqyab3w63cq4kx24olyjleh5z6zzfvyt7uqqd.onion", + "notes": "Part of: https://www.privacytools.io" + }, + { + "url": "https://td.vern.cc", + "onion": "http://td.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion", + "i2p": "http://verncco2oaxjikammz4pi7umzp673cme6zuemx7yeeewspwrw3va.b32.i2p", + "notes": "Operated by https://vern.cc" + }, + { + "url": "https://teddit.rawbit.ninja", + "onion": "http://yqu4yj5lju7bmlwpzpmltb5gsu6cw7nnbcxxx4iqemwa56nxjiggf4qd.onion", + "notes": "Operated by https://rawbit.ninja" + }, + { + "url": "https://teddit.artemislena.eu", + "onion": "http://teddit.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion", + "notes": "Operated by https://artemislena.eu" + }, + { + "url": "https://teddit.hostux.net", + "notes": "Managed by https://hostux.net" + }, + { + "url": "https://teddit.no-logs.com/", + "notes": "Managed by https://no-logs.com" + }, + { + "url": "https://teddit.projectsegfau.lt", + "onion": "http://teddit.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion", + "notes": "Maintained by Project Segfault Team (https://projectsegfau.lt/team)" + } +] diff --git a/package-lock.json b/package-lock.json index a1cf006f..96bfca54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,38 @@ { "name": "teddit", - "version": "0.3.1", + "version": "0.4.9", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==" }, "@babel/parser": { - "version": "7.13.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.11.tgz", - "integrity": "sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==" + "version": "7.16.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.6.tgz", + "integrity": "sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ==" }, "@babel/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", - "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz", + "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==", "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-validator-identifier": "^7.15.7", "to-fast-properties": "^2.0.0" } }, + "@postman/form-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", + "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "@postman/tunnel-agent": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", @@ -32,6 +41,12 @@ "safe-buffer": "^5.0.1" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -46,10 +61,33 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -57,10 +95,20 @@ "uri-js": "^4.2.2" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "asap": { "version": "2.0.6", @@ -68,9 +116,9 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "requires": { "safer-buffer": "~2.1.0" } @@ -96,9 +144,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "babel-walk": { "version": "3.0.0-canary-5", @@ -108,10 +156,16 @@ "@babel/types": "^7.9.6" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -121,26 +175,60 @@ "tweetnacl": "^0.14.3" } }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" }, "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "requires": { - "bytes": "3.1.0", + "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" } }, "brotli": { @@ -152,9 +240,9 @@ } }, "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "call-bind": { "version": "1.0.2", @@ -178,6 +266,22 @@ "is-regex": "^1.0.3" } }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -206,15 +310,14 @@ "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - } } }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, "constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", @@ -225,11 +328,18 @@ } }, "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "content-type": { @@ -238,16 +348,16 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", - "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", "requires": { - "cookie": "0.4.0", + "cookie": "0.4.1", "cookie-signature": "1.0.6" } }, @@ -283,19 +393,19 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" }, "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "doctypes": { "version": "1.1.0", @@ -314,58 +424,85 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "requires": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.0", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "extend": { @@ -388,17 +525,26 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" } }, @@ -408,14 +554,21 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true }, "function-bind": { "version": "1.1.1", @@ -440,6 +593,15 @@ "assert-plus": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -462,38 +624,76 @@ "function-bind": "^1.1.1" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, "helmet": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.2.0.tgz", - "integrity": "sha512-aoiSxXMd0ks1ojYpSCFoCRzgv4rY/uB9jKStaw8PkXwsdLYa/Gq+Nc5l0soH0cwBIsLAlujPnx4HLQs+LaXCrQ==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz", + "integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==" }, "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" } }, "http-signature": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.4.tgz", - "integrity": "sha512-CbG3io8gUSIxNNSgq+XMjgpTMzAeVRipxVXjuGrDhH5M1a2kZ03w20s8FCLR1NjnnJj10KbvabvckmtQcYNb9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "requires": { "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", + "jsprim": "^2.0.2", "sshpk": "^1.14.1" } }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -502,20 +702,35 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "requires": { "has": "^1.0.3" } @@ -529,18 +744,39 @@ "object-assign": "^4.1.1" } }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, "is-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.1" + "has-tostringtag": "^1.0.0" } }, "is-typedarray": { @@ -564,9 +800,9 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-schema-traverse": { "version": "0.4.1", @@ -579,13 +815,13 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -598,25 +834,20 @@ "promise": "^7.0.1" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "mime": { "version": "1.6.0", @@ -624,22 +855,31 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.44.0" + "mime-db": "1.51.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" } }, "minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", "requires": { "yallist": "^4.0.0" } @@ -664,9 +904,62 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, "oauth-sign": { "version": "0.9.0", @@ -678,10 +971,15 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "requires": { "ee-first": "1.1.1" } @@ -697,24 +995,30 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, "postman-request": { - "version": "2.88.1-postman.27", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.27.tgz", - "integrity": "sha512-4Qc7p3/cbp5S4Q6LcOzJ+K5N7loWDKjW0S9hj8M2AMJDUVcFUbdgvQb6ZfTERz2+34xP9ByCy7VhdnNCATe/bA==", + "version": "2.88.1-postman.30", + "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.30.tgz", + "integrity": "sha512-zsGvs8OgNeno1Q44zTgGP2IL7kCqUy4DAtl8/ms0AQpqkIoysrxzR/Zg4kM1Kz8/duBvwxt8NN717wB7SMNm6w==", "requires": { "@postman/form-data": "~3.1.1", "@postman/tunnel-agent": "^0.6.3", @@ -740,20 +1044,10 @@ "uuid": "^3.3.2" }, "dependencies": { - "@postman/form-data": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", - "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" } } }, @@ -766,11 +1060,11 @@ } }, "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "requires": { - "forwarded": "~0.1.2", + "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, @@ -779,6 +1073,12 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "pug": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz", @@ -897,9 +1197,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } }, "range-parser": { "version": "1.2.1", @@ -907,31 +1210,47 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.2", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" } }, "redis": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", - "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", "requires": { - "denque": "^1.4.1", - "redis-commands": "^1.5.0", + "denque": "^1.5.0", + "redis-commands": "^1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0" } }, "redis-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", - "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" }, "redis-errors": { "version": "1.2.0", @@ -965,48 +1284,81 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "requires": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.7.2", + "http-errors": "2.0.0", "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", + "ms": "2.1.3", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "dependencies": { "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.1" + "send": "0.18.0" } }, "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } }, "sshpk": { "version": "1.16.1", @@ -1025,9 +1377,9 @@ } }, "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, "stream-length": { "version": "1.0.2", @@ -1037,21 +1389,48 @@ "bluebird": "^2.6.2" } }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "token-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -1061,6 +1440,11 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -1075,15 +1459,21 @@ "mime-types": "~2.1.24" } }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "requires": { "punycode": "^2.1.0" } @@ -1091,7 +1481,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { "version": "3.4.0", @@ -1118,6 +1508,20 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "with": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", diff --git a/package.json b/package.json index c56547f3..339a0ef9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "teddit", - "version": "0.3.1", + "version": "0.4.9", "description": "A free and open source alternative Reddit front-end focused on privacy.", "homepage": "https://teddit.net", "bugs": { @@ -20,18 +20,22 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js" + "start": "node app.js", + "dev": "nodemon app.js" }, "dependencies": { "compression": "^1.7.4", "cookie-parser": "^1.4.5", - "express": "^4.17.1", - "helmet": "^4.2.0", + "express": "^4.18.2", + "helmet": "^4.6.0", + "https-proxy-agent": "^5.0.0", "minizlib": "^2.1.2", - "node-fetch": "^2.6.1", - "postman-request": "^2.88.1-postman.27", + "node-fetch": "^2.6.5", + "postman-request": "^2.88.1-postman.30", "pug": "^3.0.2", - "redis": "^3.0.2" + "redis": "^3.1.2" }, - "devDependencies": {} + "devDependencies": { + "nodemon": "^2.0.20" + } } diff --git a/routes.js b/routes.js deleted file mode 100644 index db6ae36c..00000000 --- a/routes.js +++ /dev/null @@ -1,1902 +0,0 @@ -/** -* Lots of routes.. would be good idea to do some separation I guess. -*/ -module.exports = (app, redis, fetch, RedditAPI) => { - const config = require('./config'); - let processSubreddit = require('./inc/processJsonSubreddit.js')(); - let processPost = require('./inc/processJsonPost.js')(); - let processUser = require('./inc/processJsonUser.js')(); - let processSearches = require('./inc/processSearchResults.js')(); - let processAbout = require('./inc/processSubredditAbout.js')(); - let tedditApiSubreddit = require('./inc/teddit_api/handleSubreddit.js')(); - let tedditApiUser = require('./inc/teddit_api/handleUser.js')(); - let processSubredditsExplore = require('./inc/processSubredditsExplore.js')(); - let processMoreComments = require('./inc/processMoreComments.js')(); - - app.all('*', (req, res, next) => { - let themeOverride = req.query.theme - if(themeOverride) { - // Convert Dark to dark since the stylesheet has it lower case - themeOverride = themeOverride.toLowerCase() - // This override here will set it for the current request - req.cookies.theme = themeOverride - // this will set it for future requests - res.cookie('theme', themeOverride, { maxAge: 31536000, httpOnly: true }) - } else if(!req.cookies.theme && req.cookies.theme !== '') { - req.cookies.theme = config.theme - } - - let flairsOverride = req.query.flairs - if(flairsOverride) { - req.cookies.flairs = flairsOverride - res.cookie('flairs', flairsOverride, { maxAge: 31536000, httpOnly: true }) - } - - let nsfwEnabledOverride = req.query.nsfw_enabled - if(nsfwEnabledOverride) { - req.cookies.nsfw_enabled = nsfwEnabledOverride - res.cookie('nsfw_enabled', nsfwEnabledOverride, { maxAge: 31536000, httpOnly: true }) - } - - let highlightControversialOverride = req.query.highlight_controversial - if(highlightControversialOverride) { - req.cookies.highlight_controversial = highlightControversialOverride - res.cookie('highlight_controversial', highlightControversialOverride, { maxAge: 31536000, httpOnly: true }) - } - - let postMediaMaxHeight = req.query.post_media_max_height - if(postMediaMaxHeight) { - if(config.post_media_max_heights.hasOwnProperty(postMediaMaxHeight) || !isNaN(postMediaMaxHeight)) { - req.cookies.post_media_max_height = postMediaMaxHeight - res.cookie('post_media_max_height', postMediaMaxHeight, { maxAge: 31536000, httpOnly: true }) - } - } - - let collapseChildComments = req.query.collapse_child_comments - if(collapseChildComments) { - req.cookies.collapse_child_comments = collapseChildComments - res.cookie('collapse_child_comments', collapseChildComments, { maxAge: 31536000, httpOnly: true }) - } - - let showUpvotedPercentage = req.query.show_upvoted_percentage - if(showUpvotedPercentage) { - req.cookies.show_upvoted_percentage = showUpvotedPercentage - res.cookie('show_upvoted_percentage', showUpvotedPercentage, { maxAge: 31536000, httpOnly: true }) - } - - let domainTwitter = req.query.domain_twitter - if(domainTwitter) { - req.cookies.domain_twitter = domainTwitter - res.cookie('domain_twitter', domainTwitter, { maxAge: 31536000, httpOnly: true }) - } - - let domainYoutube = req.query.domain_youtube - if(domainYoutube) { - req.cookies.domain_youtube = domainYoutube - res.cookie('domain_youtube', domainYoutube, { maxAge: 31536000, httpOnly: true }) - } - - let domainInstagram = req.query.domain_instagram - if(domainInstagram) { - req.cookies.domain_instagram = domainInstagram - res.cookie('domain_instagram', domainInstagram, { maxAge: 31536000, httpOnly: true }) - } - - const valid_reddit_starts = ['/https://old.reddit.com', '/https://reddit.com', '/https://www.reddit.com', '/old.reddit.com', '/reddit.com', '/www.reddit.com'] - for(var i = 0; i < valid_reddit_starts.length; i++) { - if(req.url.startsWith(valid_reddit_starts[i])) { - req.url = req.url.substring(1) - const redditRegex = /([A-z.]+\.)?(reddit(\.com))/gm; - let teddified_url = req.url.replace(redditRegex, '') - if(teddified_url.includes('://')) { - teddified_url = teddified_url.split('://')[1] - } - if(teddified_url == '') { - teddified_url = '/' - } - return res.redirect(teddified_url) - } - } - - if(!config.rate_limiting) { - return next() - } - - if(config.rate_limiting.enabled) { - /** - * This route enforces request limits based on an IP address if - * config.rate_limiting.enabled is true. By default it's false. - */ - - let ip = String(req.headers['x-forwarded-for'] || req.connection.remoteAddress || 'unknown') - - if(ip === 'unknown') { - return next() - } - - if(ratelimit_counts[ip] == undefined) { - ratelimit_counts[ip] = 0 - } - - if(ratelimit_timestamps[ip] == undefined) { - ratelimit_timestamps[ip] = Date.now() - } - - let diff = Date.now() - ratelimit_timestamps[ip] - let credit = (diff / 60000) * config.rate_limiting.limit_after_limited - ratelimit_counts[ip] -= credit - - if(ratelimit_counts[ip] < 0) { - ratelimit_counts[ip] = 0 - } - - ratelimit_counts[ip]++ - ratelimit_timestamps[ip] = Date.now() - - if(ratelimit_counts[ip] > config.rate_limiting.initial_limit) { - console.log(`RATE LIMITED IP ADDRESS: ${ip}`) - return res.send(`Hold your horses! You have hit the request limit. You should be able to refresh this page in a couple of seconds. If you think you are wrongfully limited, create an issue at https://codeberg.org/teddit/teddit. Rate limiting is highly experimental feature.`) - } else { - return next() - } - } else { - return next() - } - }) - - app.get('/about', (req, res, next) => { - return res.render('about', { user_preferences: req.cookies }) - }) - - app.get('/preferences', (req, res, next) => { - return res.render('preferences', { user_preferences: req.cookies, instance_config: config }) - }) - - app.get('/resetprefs', (req, res, next) => { - res.clearCookie('theme') - res.clearCookie('flairs') - res.clearCookie('nsfw_enabled') - res.clearCookie('highlight_controversial') - res.clearCookie('subbed_subreddits') - return res.redirect('/preferences') - }) - - app.get('/import_prefs/:key', (req, res, next) => { - let key = req.params.key - if(!key) - return res.redirect('/') - if(key.length !== 10) - return res.redirect('/') - - key = `prefs_key:${key}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the preferences import key ${key} from redis.`, error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - try { - let prefs = JSON.parse(json) - let subbed_subreddits_is_set = false - for(var setting in prefs) { - if(prefs.hasOwnProperty(setting)) { - res.cookie(setting, prefs[setting], { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - if(setting === 'subbed_subreddits') - subbed_subreddits_is_set = true - } - } - if(!subbed_subreddits_is_set) - res.clearCookie('subbed_subreddits') - return res.redirect('/') - } catch(e) { - console.error(`Error setting imported preferences to the cookies. Key: ${key}.`, error) - } - } else { - return res.redirect('/preferences') - } - }) - }) - - app.get('/privacy', (req, res, next) => { - return res.render('privacypolicy', { user_preferences: req.cookies }) - }) - - app.get('/gallery/:id', (req, res, next) => { - return res.redirect(`/comments/${req.params.id}`) - }) - - app.get('/poll/:id', (req, res, next) => { - return res.redirect(`/comments/${req.params.id}`) - }) - - app.get('/saved', (req, res, next) => { - let saved = req.cookies.saved - - if(!saved || !Array.isArray(saved)) { - return res.render('saved', { - json: null, - user_preferences: req.cookies, - }) - } - - let key = `saved_posts:${saved.join(',')}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting saved_post ${saved_post} key from redis.`, error) - return res.redirect('/') - } - if(json) { - (async () => { - let processed_json = await processJsonSubreddit(json, 'redis', null, req.cookies, true) - if(!processed_json.error) { - return res.render('saved', { - json: processed_json, - user_preferences: req.cookies, - }) - } else { - return res.render('subreddit', { - json: null, - error: true, - data: processed_json, - user_preferences: req.cookies - }) - } - })() - } - }) - }) - - app.get('/save/:id', (req, res, next) => { - let post_id = req.params.id - let redis_key = req.query.rk - let back = req.query.b - let saved = req.cookies.saved - let fetched = req.query.f - - if(!post_id || !redis_key) - return res.redirect('/saved') - - if(!saved || !Array.isArray(saved)) - saved = [] - - if(saved.length > 100) - return res.send('You can not save more than 100 posts.') - - redis.get(redis_key, (error, json) => { - if(error) { - console.error(`Error getting the ${redis_key} key from redis (via /save/).`, error) - return res.redirect('/') - } - if(json) { - json = JSON.parse(json) - if(fetched === 'true' || redis_key.includes('/comments/')) - json = json[0] - - let post_to_save = false - for(var i = 0; i < json.data.children.length; i++) { - let post = json.data.children[i] - if(post.data.id === post_id) { - post_to_save = post - break - } - } - - if(post_to_save) { - if(!saved || !Array.isArray(saved)) - saved = [] - - for(var i = 0; i < saved.length; i++) { - if(post_to_save.data.id === saved[i]) - return res.redirect('/saved') - } - - let key = `saved_posts:${saved.join(',')}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting saved_posts ${key} key from redis.`, error) - return res.redirect('/') - } - links = JSON.parse(json) - if(!links) - links = [] - - links.unshift(post_to_save) - saved.unshift(post_to_save.data.id) - res.cookie('saved', saved, { maxAge: 3 * 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - let new_key = `saved_posts:${saved.join(',')}` - redis.set(new_key, JSON.stringify(links), (error) => { - if(error) - console.error(`Error saving ${new_key} to redis.`, error) - - if(!back) - return res.redirect('/saved') - else { - back = back.replace(/§2/g, '?').replace(/§1/g, '&') - return res.redirect(back) - } - }) - }) - } else { - return res.redirect(`/comments/${post_id}/?save=true&b=${back}`) - } - } else { - return res.redirect(`/comments/${post_id}/?save=true&b=${back}`) - } - }) - }) - - app.get('/unsave/:id', (req, res, next) => { - let post_id = req.params.id - let back = req.query.b - let saved = req.cookies.saved - - if(!post_id) - return res.redirect('/saved') - - if(!saved || !Array.isArray(saved)) - return res.redirect('/saved') - - let key = `saved_posts:${saved.join(',')}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the ${key} key from redis (via /save/).`, error) - return res.redirect('/') - } - if(json) { - json = JSON.parse(json) - let post_found = false - for(var i = 0; i < json.length; i++) { - if(json[i].data.id === post_id) { - post_found = true - json.splice(i, 1) - for(var j = 0; j < saved.length; j++) { - if(saved[j] === post_id) - saved.splice(j, 1) - } - } - } - if(post_found) { - res.cookie('saved', saved, { maxAge: 3 * 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - let new_key = `saved_posts:${saved.join(',')}` - redis.set(new_key, JSON.stringify(json), (error) => { - if(error) - console.error(`Error saving ${new_key} to redis.`, error) - - if(!back) - return res.redirect('/saved') - else { - back = back.replace(/§2/g, '?').replace(/§1/g, '&') - return res.redirect(back) - } - }) - } else { - return res.redirect(`/saved`) - } - } else { - return res.redirect(`/saved`) - } - }) - }) - - app.get('/subreddits/:sort?', (req, res, next) => { - let q = req.query.q - let nsfw = req.query.nsfw - let after = req.query.after - let before = req.query.before - let sortby = req.params.sort - let searching = false - - if(!after) { - after = '' - } - if(!before) { - before = '' - } - - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - - if(nsfw !== 'on') { - nsfw = 'off' - } - - if(!sortby) { - sortby = '' - } - - let key = `subreddits:sort:${sortby}${d}` - - if(sortby === 'search') { - if(typeof(q) == 'undefined' || q == '') - return res.redirect('/subreddits') - - key = `subreddits:search:q:${q}:nsfw:${nsfw}${d}` - searching = true - } - - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the subreddits key from redis.`, error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log(`Got subreddits key from redis.`); - (async () => { - let processed_json = await processJsonSubredditsExplore(json, 'redis', null, req.cookies) - if(!processed_json.error) { - return res.render('subreddits_explore', { - json: processed_json, - sortby: sortby, - after: after, - before: before, - q: q, - nsfw: nsfw, - searching: searching, - subreddits_front: (!before && !after) ? true : false, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled - }) - } else { - return res.render('subreddits_explore', { - json: null, - error: true, - data: processed_json, - user_preferences: req.cookies - }) - } - })() - } else { - let url = '' - if(config.use_reddit_oauth) { - if(!searching) - url = `https://oauth.reddit.com/subreddits/${sortby}?api_type=json&count=25&g=GLOBAL&t=${d}` - else - url = `https://oauth.reddit.com/subreddits/search?api_type=json&q=${q}&include_over_18=${nsfw}${d}` - } else { - if(!searching) - url = `https://reddit.com/subreddits/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${d}` - else - url = `https://reddit.com/subreddits/search.json?api_type=json&q=${q}&include_over_18=${nsfw}${d}` - } - - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - let ex = config.setexs.subreddits_explore.front - if(sortby === 'new') - ex = config.setexs.subreddits_explore.new_page - redis.setex(key, ex, JSON.stringify(json), (error) => { - if(error) { - console.error(`Error setting the subreddits key to redis.`, error) - return res.render('subreddits_explore', { json: null, user_preferences: req.cookies }) - } else { - console.log(`Fetched the JSON from reddit.com/subreddits.`); - (async () => { - let processed_json = await processJsonSubredditsExplore(json, 'from_online', null, req.cookies) - return res.render('subreddits_explore', { - json: processed_json, - sortby: sortby, - after: after, - before: before, - q: q, - nsfw: nsfw, - searching: searching, - subreddits_front: (!before && !after) ? true : false, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled - }) - })() - } - }) - }) - } else { - if(result.status === 404) { - console.log('404 – Subreddits not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the JSON file from reddit.com/subreddits.`, error) - }) - } - }) - }) - - app.get('/subscribe/:subreddit', (req, res, next) => { - let subreddit = req.params.subreddit - let subbed = req.cookies.subbed_subreddits - let back = req.query.b - - if(!subreddit) - return res.redirect('/') - - if(!subbed || !Array.isArray(subbed)) - subbed = [] - - if(!subbed.includes(subreddit)) - subbed.push(subreddit) - - res.cookie('subbed_subreddits', subbed, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(!back) - return res.redirect('/r/' + subreddit) - else { - back = back.replace(/,/g, '+').replace(/§1/g, '&') - return res.redirect(back) - } - }) - - app.get('/import_subscriptions/:subreddits', (req, res, next) => { - let subreddits = req.params.subreddits - let subbed = req.cookies.subbed_subreddits - let back = req.query.b - - if(!subreddits) - return res.redirect('/') - - if(!subbed || !Array.isArray(subbed)) - subbed = [] - - subreddits = subreddits.split('+') - for(var i = 0; i < subreddits.length; i++) { - if(!subbed.includes(subreddits[i])) - subbed.push(subreddits[i]) - } - - res.cookie('subbed_subreddits', subbed, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(!back) - return res.redirect('/r/' + subreddits) - else { - back = back.replace(/,/g, '+').replace(/ /g, '+') - return res.redirect(back) - } - }) - - app.get('/unsubscribe/:subreddit', (req, res, next) => { - let subreddit = req.params.subreddit - let subbed = req.cookies.subbed_subreddits - let back = req.query.b - - if(!subreddit || !subbed || !Array.isArray(subbed)) { - res.clearCookie('subbed_subreddits') - return res.redirect('/') - } - - var index = subbed.indexOf(subreddit) - if(index !== -1) - subbed.splice(index, 1) - - if(subbed.length <= 0) - res.clearCookie('subbed_subreddits') - else - res.cookie('subbed_subreddits', subbed, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(!back) - return res.redirect('/r/' + subreddit) - else { - back = back.replace(/,/g, '+').replace(/§1/g, '&') - return res.redirect(back) - } - }) - - app.get('/search', (req, res, next) => { - let q = req.query.q - - if (typeof q === "undefined") { - return res.render('search', { - json: { posts: [] }, - no_query: true, - q: '', - restrict_sr: undefined, - nsfw: undefined, - subreddit: 'all', - sortby: undefined, - past: undefined, - user_preferences: req.cookies - }) - } - - let restrict_sr = req.query.restrict_sr - let nsfw = req.query.nsfw - let sortby = req.query.sort - let past = req.query.t - let after = req.query.after - let before = req.query.before - if(!after) { - after = '' - } - if(!before) { - before = '' - } - if(restrict_sr !== 'on') { - restrict_sr = 'off' - } - - if(nsfw !== 'on') { - nsfw = 'off' - } - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - return res.redirect(`/r/all/search?q=${q}&restrict_sr=${restrict_sr}&nsfw=${nsfw}&sort=${sortby}&t=${past}${d}`) - }) - - app.get('/:sort?', async (req, res, next) => { - let past = req.query.t - let before = req.query.before - let after = req.query.after - let sortby = req.params.sort || '' - let api_req = req.query.api - let api_type = req.query.type - let api_target = req.query.target - - let proxyable = (sortby.includes('.jpg') || sortby.includes('.png') || sortby.includes('.jpeg')) ? true : false - if(proxyable) { - let params = new URLSearchParams(req.query).toString() - let image_url = `https://preview.redd.it/${sortby}?${params}` - let proxied_image = await downloadAndSave(image_url) - if(proxied_image) { - return res.redirect(proxied_image) - } else { - return res.redirect('/') - } - } - - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - - if(sortby == '') { - sortby = 'hot' - } - - if(['apple-touch-icon.png', 'apple-touch-icon-precomposed.png', 'apple-touch-icon-120x120.png', 'apple-touch-icon-120x120-precomposed.png'].includes(sortby)) { - return res.sendStatus(404) // return 404 on shitty apple favicon stuff - } - - if(!['new', 'rising', 'controversial', 'top', 'gilded', 'hot'].includes(sortby)) { - console.log(`Got invalid sort.`, req.originalUrl) - return res.redirect('/') - } - - if(past) { - if(sortby === 'controversial' || sortby === 'top') { - if(!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { - console.error(`Got invalid past.`, req.originalUrl) - return res.redirect(`/`) - } - } else { - past = undefined - } - } else { - if(sortby === 'controversial' || sortby === 'top') { - past = 'day' - } - } - - if(req.query.hasOwnProperty('api')) - api_req = true - else - api_req = false - - let key = `/after:${after}:before:${before}:sort:${sortby}:past:${past}` - - let subbed_subreddits = req.cookies.subbed_subreddits - let get_subbed_subreddits = false - if(subbed_subreddits && Array.isArray(subbed_subreddits)) { - get_subbed_subreddits = true - subbed_subreddits = subbed_subreddits.join('+') - key = `${subbed_subreddits.toLowerCase()}:${after}:${before}:sort:${sortby}:past:${past}` - } - - redis.get(key, (error, json) => { - if(error) { - console.error('Error getting the frontpage key from redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log('Got frontpage key from redis.'); - (async () => { - if(api_req) { - return handleTedditApiSubreddit(json, req, res, 'redis', api_type, api_target, '/') - } else { - let processed_json = await processJsonSubreddit(json, 'redis', null, req.cookies) - return res.render('index', { - json: processed_json, - sortby: sortby, - past: past, - user_preferences: req.cookies, - redis_key: key - }) - } - })() - } else { - let url = '' - if(config.use_reddit_oauth) { - if(get_subbed_subreddits) - url = `https://oauth.reddit.com/r/${subbed_subreddits}/${sortby}?api_type=json&count=25&g=GLOBAL&t=${past}${d}` - else - url = `https://oauth.reddit.com/${sortby}?api_type=json&g=GLOBAL&t=${past}${d}` - } else { - if(get_subbed_subreddits) - url = `https://reddit.com/r/${subbed_subreddits}/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${past}${d}` - else - url = `https://reddit.com/${sortby}.json?g=GLOBAL&t=${past}${d}` - } - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - redis.setex(key, config.setexs.frontpage, JSON.stringify(json), (error) => { - if(error) { - console.error('Error setting the frontpage key to redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } else { - console.log('Fetched the frontpage from Reddit.'); - (async () => { - if(api_req) { - return handleTedditApiSubreddit(json, req, res, 'from_online', api_type, api_target, '/') - } else { - let processed_json = await processJsonSubreddit(json, 'from_online', null, req.cookies) - return res.render('index', { - json: processed_json, - sortby: sortby, - past: past, - user_preferences: req.cookies, - redis_key: key - }) - } - })() - } - }) - }) - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error('Error fetching the frontpage JSON file.', error) - }) - } - }) - }) - - app.get('/comments/:post_id/:comment?/:comment_id?', (req, res, next) => { - let post_id = req.params.post_id - let comment = req.params.comment - let comment_id = req.params.comment_id - let back = req.query.b - let save = req.query.save - let post_url = false - let comment_url = false - - if(comment) - if(comment !== 'comment' || !comment_id) - return res.redirect('/') - - if(comment) - comment_url = true - else - post_url = true - - let key = `/shorturl:post:${post_id}:comment:${comment_id}` - redis.get(key, (error, json) => { - if(error) { - console.error('Error getting the short URL for post key from redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log('Got short URL for post key from redis.') - json = JSON.parse(json) - if(post_url) { - if(save === 'true') - return res.redirect(`/save/${post_id}/?rk=${key}&b=${back}&f=true`) - return res.redirect(json[0].data.children[0].data.permalink) - } else { - return res.redirect(json[1].data.children[0].data.permalink) - } - } else { - let url = '' - if(config.use_reddit_oauth) { - if(post_url) - url = `https://oauth.reddit.com/comments/${post_id}?api_type=json` - else - url = `https://oauth.reddit.com/comments/${post_id}/comment/${comment_id}?api_type=json` - } else { - if(post_url) - url = `https://reddit.com/comments/${post_id}.json?api_type=json` - else - url = `https://reddit.com/comments/${post_id}/comment/${comment_id}.json?api_type=json` - } - - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - redis.setex(key, config.setexs.shorts, JSON.stringify(json), (error) => { - if(error) { - console.error('Error setting the short URL for post key to redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } else { - console.log('Fetched the short URL for post from Reddit.') - if(post_url) { - if(save === 'true') - return res.redirect(`/save/${post_id}/?rk=${key}&b=${back}&f=true`) - return res.redirect(json[0].data.children[0].data.permalink) - } else { - return res.redirect(json[1].data.children[0].data.permalink) - } - } - }) - }) - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error('Error fetching the short URL for post with sortby JSON file.', error) - }) - } - }) - }) - - app.get('/r/:subreddit/search', (req, res, next) => { - let subreddit = req.params.subreddit - let q = req.query.q - - if (typeof q === "undefined") { - return res.render('search', { - json: { posts: [] }, - no_query: true, - q: '', - restrict_sr: undefined, - nsfw: undefined, - subreddit: subreddit, - sortby: undefined, - past: undefined, - user_preferences: req.cookies - }) - } - - let restrict_sr = req.query.restrict_sr - let nsfw = req.query.nsfw - let sortby = req.query.sort - let past = req.query.t - let after = req.query.after - let before = req.query.before - if(!after) { - after = '' - } - if(!before) { - before = '' - } - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - - if(restrict_sr !== 'on') { - restrict_sr = 'off' - } - - if(nsfw !== 'on') { - nsfw = 'off' - } - - let key = `search:${subreddit}:${q}:${restrict_sr}:${sortby}:${past}:${after}:${before}:${nsfw}` - redis.get(key, (error, json) => { - if(error) { - console.error('Error getting the search key from redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log('Got search key from redis.'); - (async () => { - let processed_json = await processSearchResults(json, false, after, before, req.cookies) - return res.render('search', { - json: processed_json, - no_query: false, - q: q, - restrict_sr: restrict_sr, - nsfw: nsfw, - subreddit: subreddit, - sortby: sortby, - past: past, - user_preferences: req.cookies - }) - })() - } else { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com/r/${subreddit}/search?api_type=json&q=${q}&restrict_sr=${restrict_sr}&include_over_18=${nsfw}&sort=${sortby}&t=${past}${d}` - else - url = `https://reddit.com/r/${subreddit}/search.json?api_type=json&q=${q}&restrict_sr=${restrict_sr}&include_over_18=${nsfw}&sort=${sortby}&t=${past}${d}` - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - (async () => { - /** - * Fetch suggested subreddits when the restrict_sr option is - * turned off ("limit my search to") and we are on the first search - * page (just like in Reddit). - */ - json.suggested_subreddits = {} - if(restrict_sr === 'off' && before == '' && after == '') { - let url = `https://reddit.com/subreddits/search.json?q=${q}&include_over_18=${nsfw}&limit=3` - const response = await fetch(encodeURI(url)) - const data = await response.json() - json.suggested_subreddits = data - } - - redis.setex(key, config.setexs.searches, JSON.stringify(json), (error) => { - if(error) { - console.error('Error setting the searches key to redis.', error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } else { - console.log('Fetched search results from Reddit.'); - (async () => { - let processed_json = await processSearchResults(json, true, after, before, req.cookies) - return res.render('search', { - no_query: false, - json: processed_json, - q: q, - restrict_sr: restrict_sr, - nsfw: nsfw, - subreddit: subreddit, - sortby: sortby, - past: past, - user_preferences: req.cookies - }) - })() - } - }) - })() - }) - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error('Error fetching the frontpage JSON file.', error) - }) - } - }) - }) - - app.get('/r/:subreddit/wiki/:page?/:sub_page?', (req, res, next) => { - let subreddit = req.params.subreddit - let page = req.params.page - let sub_page = req.params.sub_page || '' - - if(!page) - page = 'index' - - if(sub_page != '') - sub_page = `/${sub_page}` - - function formatWikipagelisting(json, subreddit) { - let html = '
    ' - if(json.kind === 'wikipagelisting' && json.data) { - for(var i = 0; i < json.data.length; i++) { - let d = json.data[i] - html += `
  • ${d}
  • ` - } - } - html += '
' - return html - } - - let key = `${subreddit.toLowerCase()}:wiki:page:${page}:sub_page:${sub_page}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the ${subreddit} wiki key from redis.`, error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log(`Got /r/${subreddit} wiki key from redis.`) - json = JSON.parse(json) - return res.render('subreddit_wiki', { - content_html: (page !== 'pages' ? unescape(json.data.content_html) : formatWikipagelisting(json, subreddit)), - subreddit: subreddit, - user_preferences: req.cookies - }) - } else { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com/r/${subreddit}/wiki/${page}${sub_page}?api_type=json` - else - url = `https://reddit.com/r/${subreddit}/wiki/${page}${sub_page}.json?api_type=json` - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - redis.setex(key, config.setexs.wikis, JSON.stringify(json), (error) => { - if(error) { - console.error(`Error setting the ${subreddit} wiki key to redis.`, error) - return res.render('subreddit', { json: null, user_preferences: req.cookies }) - } else { - console.log(`Fetched the JSON from reddit.com/r/${subreddit}/wiki.`) - return res.render('subreddit_wiki', { - content_html: (page !== 'pages' ? unescape(json.data.content_html) : formatWikipagelisting(json, subreddit)), - subreddit: subreddit, - user_preferences: req.cookies - }) - } - }) - }) - } else { - if(result.status === 404) { - console.log('404 – Subreddit wiki not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the JSON file from reddit.com/r/${subreddit}/wiki.`, error) - }) - } - }) - }) - - app.get('/r/:subreddit/w/:page?/:sub_page?', (req, res, next) => { - /* "w" is a shorturl for wikis for example https://old.reddit.com/r/privacytoolsIO/w/index */ - let subreddit = req.params.subreddit - let page = req.params.page - let sub_page = req.params.sub_page || '' - - if(!page) - page = 'index' - - if(sub_page != '') - sub_page = `/${sub_page}` - - return res.redirect(`/r/${subreddit}/wiki/${page}${sub_page}`) - }) - - app.get('/r/random', (req, res, next) => { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com/r/random?api_type=json&count=25&g=GLOBAL` - else - url = `https://reddit.com/r/random.json?api_type=json&count=25&g=GLOBAL` - - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - let subreddit = json.data.children[0].data.subreddit - if(subreddit) { - let key = `${subreddit.toLowerCase()}:undefined:undefined:sort:hot:past:undefined` - redis.setex(key, config.setexs.subreddit, JSON.stringify(json), (error) => { - if(error) { - console.error(`Error setting the random subreddit key to redis.`, error) - return res.render('subreddit', { json: null, user_preferences: req.cookies }) - } else { - console.log(`Fetched the JSON from reddit.com/r/${subreddit}.`); - return res.redirect(`/r/${subreddit}`) - } - }) - } else { - console.error(`Fetching random subreddit failed.`, json) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - }) - } else { - if(result.status === 404) { - console.log('404 – Subreddit not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the JSON file from reddit.com/r/random.`, error) - }) - }) - - app.get('/r/:subreddit/:sort?', (req, res, next) => { - let subreddit = req.params.subreddit - let sortby = req.params.sort - let past = req.query.t - let before = req.query.before - let after = req.query.after - let api_req = req.query.api - let api_type = req.query.type - let api_target = req.query.target - - if(req.query.hasOwnProperty('api')) - api_req = true - else - api_req = false - - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - - if(!sortby) { - sortby = 'hot' - } - - if(!['new', 'rising', 'controversial', 'top', 'gilded', 'hot'].includes(sortby)) { - console.log(`Got invalid sort.`, req.originalUrl) - return res.redirect(`/r/${subreddit}`) - } - - if(past) { - if(sortby === 'controversial' || sortby === 'top') { - if(!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { - console.error(`Got invalid past.`, req.originalUrl) - return res.redirect(`/r/${subreddit}/${sortby}`) - } - } else { - past = undefined - } - } else { - if(sortby === 'controversial' || sortby === 'top') { - past = 'day' - } - } - - let key = `${subreddit.toLowerCase()}:${after}:${before}:sort:${sortby}:past:${past}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the ${subreddit} key from redis.`, error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log(`Got /r/${subreddit} key from redis.`); - (async () => { - if(api_req) { - return handleTedditApiSubreddit(json, req, res, 'redis', api_type, api_target, subreddit) - } else { - let processed_json = await processJsonSubreddit(json, 'redis', null, req.cookies) - let subreddit_about = await processSubredditAbout(subreddit, redis, fetch, RedditAPI) - if(!processed_json.error) { - return res.render('subreddit', { - json: processed_json, - subreddit: subreddit, - subreddit_about: subreddit_about, - subreddit_front: (!before && !after) ? true : false, - sortby: sortby, - past: past, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled, - redis_key: key, - after: req.query.after, - before: req.query.before - }) - } else { - return res.render('subreddit', { - json: null, - error: true, - data: processed_json, - user_preferences: req.cookies - }) - } - } - })() - } else { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com/r/${subreddit}/${sortby}?api_type=json&count=25&g=GLOBAL&t=${past}${d}` - else - url = `https://reddit.com/r/${subreddit}/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${past}${d}` - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - redis.setex(key, config.setexs.subreddit, JSON.stringify(json), (error) => { - if(error) { - console.error(`Error setting the ${subreddit} key to redis.`, error) - return res.render('subreddit', { json: null, user_preferences: req.cookies }) - } else { - console.log(`Fetched the JSON from reddit.com/r/${subreddit}.`); - (async () => { - if(api_req) { - return handleTedditApiSubreddit(json, req, res, 'from_online', api_type, api_target, subreddit) - } else { - let processed_json = await processJsonSubreddit(json, 'from_online', null, req.cookies) - let subreddit_about = await processSubredditAbout(subreddit, redis, fetch, RedditAPI) - return res.render('subreddit', { - json: processed_json, - subreddit: subreddit, - subreddit_about: subreddit_about, - subreddit_front: (!before && !after) ? true : false, - sortby: sortby, - past: past, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled, - redis_key: key, - after: req.query.after, - before: req.query.before - }) - } - })() - } - }) - }) - } else { - if(result.status === 404) { - console.log('404 – Subreddit not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the JSON file from reddit.com/r/${subreddit}.`, error) - }) - } - }) - }) - - app.get('/r/:subreddit/comments/:id/:snippet?/:comment_id?', (req, res, next) => { - let subreddit = req.params.subreddit - let id = req.params.id - let snippet = encodeURIComponent(req.params.snippet) - let sortby = req.query.sort - let comment_id = '' - let viewing_comment = false - let comment_ids = req.query.comment_ids - let context = parseInt(req.query.context) - - if(req.params.comment_id) { - comment_id = `${req.params.comment_id}/` - viewing_comment = true - } - - if(!sortby) { - sortby = config.post_comments_sort - } - - if(!['confidence', 'top', 'new', 'controversial', 'old', 'qa', 'random'].includes(sortby)) { - console.log(`Got invalid sort.`, req.originalUrl) - return res.redirect('/') - } - - let comments_url = `/r/${subreddit}/comments/${id}/${snippet}/${comment_id}` - let post_url = `/r/${subreddit}/comments/${id}/${snippet}/` - let comments_key = `${comments_url}:sort:${sortby}` - - redis.get(comments_key, (error, json) => { - if(error) { - console.error(`Error getting the ${comments_url} key from redis.`, error) - return res.render('index', { post: null, user_preferences: req.cookies }) - } - if(json) { - console.log(`Got ${comments_url} key from redis.`); - (async () => { - let parsed = false - let more_comments = null - if(comment_ids) { - let key = `${post_url}:morechildren:comment_ids:${comment_ids}` - more_comments = await moreComments(fetch, redis, post_url, comment_ids, id) - - if(more_comments === false) { - return res.redirect(post_url) - } else { - json = JSON.parse(json) - json[1].data.children = more_comments - parsed = true - } - } - - let processed_json = await processJsonPost(json, parsed, req.cookies) - let finalized_json = await finalizeJsonPost(processed_json, id, post_url, more_comments, viewing_comment, req.cookies) - return res.render('post', { - post: finalized_json.post_data, - comments: finalized_json.comments, - viewing_comment: viewing_comment, - post_url: post_url, - subreddit: subreddit, - sortby: sortby, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled, - post_media_max_heights: config.post_media_max_heights, - redis_key: comments_key - }) - })() - } else { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com${comments_url}?api_type=json&sort=${sortby}&context=${context}` - else - url = `https://reddit.com${comments_url}.json?api_type=json&sort=${sortby}&context=${context}` - - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - redis.setex(comments_key, config.setexs.posts, JSON.stringify(json), (error) => { - if(error) { - console.error(`Error setting the ${comments_url} key to redis.`, error) - return res.render('post', { post: null, user_preferences: req.cookies }) - } else { - console.log(`Fetched the JSON from reddit.com${comments_url}.`); - (async () => { - let more_comments = null - if(comment_ids) { - let key = `${post_url}:morechildren:comment_ids:${comment_ids}` - more_comments = await moreComments(fetch, redis, post_url, comment_ids, id) - - if(more_comments === false) { - return res.redirect(post_url) - } else { - json[1].data.children = more_comments - } - } - - let processed_json = await processJsonPost(json, true, req.cookies) - let finalized_json = await finalizeJsonPost(processed_json, id, post_url, more_comments, viewing_comment) - return res.render('post', { - post: finalized_json.post_data, - comments: finalized_json.comments, - viewing_comment: viewing_comment, - post_url: post_url, - subreddit: subreddit, - sortby: sortby, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled, - post_media_max_heights: config.post_media_max_heights, - redis_key: comments_key - }) - })() - } - }) - }) - } else { - if(result.status === 404) { - console.log('404 – Post not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - http_statustext: result.statusText, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the JSON file from reddit.com${comments_url}.`, error) - }) - } - }) - }) - - app.get('/user/:user/:kind?', (req, res, next) => { - let kind = '' - if(req.params.kind) - kind = `/${req.params.kind}` - let q = '' - if(req.query.sort) - q += `?sort=${req.query.sort}&` - if(req.query.t) - q += `t=${req.query.t}` - - res.redirect(`/u/${req.params.user}${kind}${q}`) - }) - - app.get('/u/:user/:kind?', (req, res, next) => { - let user = req.params.user - let after = req.query.after - let before = req.query.before - let post_type = req.params.kind - let kind = post_type - let user_data = {} - let api_req = req.query.api - let api_type = req.query.type - let api_target = req.query.target - - if(req.query.hasOwnProperty('api')) - api_req = true - else - api_req = false - - if(!after) { - after = '' - } - if(!before) { - before = '' - } - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - - post_type = `/${post_type}` - switch(post_type) { - case '/comments': - kind = 't1' - break; - case '/submitted': - kind = 't3' - break; - default: - post_type = '' - kind = '' - } - - let sortby = req.query.sort - let past = req.query.t - - if(!sortby) { - sortby = 'new' - } - - if(!['hot', 'new', 'controversial', 'top'].includes(sortby)) { - console.log(`Got invalid sort.`, req.originalUrl) - return res.redirect(`/u/${user}`) - } - - if(past) { - if(sortby === 'controversial' || sortby === 'top') { - if(!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { - console.error(`Got invalid past.`, req.originalUrl) - return res.redirect(`/u/${user}/${sortby}`) - } - } else { - past = '' - } - } else { - if(sortby === 'controversial' || sortby === 'top') { - past = 'all' - } else { - past = '' - } - } - - let key = `${user}:${after}:${before}:sort:${sortby}:past:${past}:post_type:${post_type}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the user ${key} key from redis.`, error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log(`Got user ${user} key from redis.`); - (async () => { - if(api_req) { - return handleTedditApiUser(json, req, res, 'redis', api_type, api_target, user, after, before) - } else { - let processed_json = await processJsonUser(json, false, after, before, req.cookies, kind, post_type) - return res.render('user', { - data: processed_json, - sortby: sortby, - past: past, - user_preferences: req.cookies - }) - } - })() - } else { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com/user/${user}/about` - else - url = `https://reddit.com/user/${user}/about.json` - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - user_data.about = json - let url = '' - if(config.use_reddit_oauth) { - let endpoint = '/overview' - if(post_type !== '') - endpoint = post_type - url = `https://oauth.reddit.com/user/${user}${post_type}?limit=26${d}&sort=${sortby}&t=${past}` - } else { - url = `https://reddit.com/user/${user}${post_type}.json?limit=26${d}&sort=${sortby}&t=${past}` - } - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - user_data.overview = json - redis.setex(key, config.setexs.user, JSON.stringify(user_data), (error) => { - if(error) { - console.error(`Error setting the user ${key} key to redis.`, error) - return res.render('index', { post: null, user_preferences: req.cookies }) - } else { - (async () => { - if(api_req) { - return handleTedditApiUser(user_data, req, res, 'online', api_type, api_target, user, after, before) - } else { - let processed_json = await processJsonUser(user_data, true, after, before, req.cookies, kind, post_type) - return res.render('user', { - data: processed_json, - sortby: sortby, - past: past, - user_preferences: req.cookies - }) - } - })() - } - }) - }) - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the overview JSON file from reddit.com/u/${user}`, error) - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - }) - }) - } else { - if(result.status === 404) { - console.log('404 – User not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - http_statustext: result.statusText, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the about JSON file from reddit.com/u/${user}`, error) - }) - } - }) - }) - - app.get('/user/:user/m/:custom_feed', (req, res, next) => { - res.redirect(`/u/${req.params.user}/m/${req.params.custom_feed}`) - }) - - app.get('/u/:user/m/:custom_feed/:sort?', (req, res, next) => { - let user = req.params.user - let custom_feed = req.params.custom_feed - let subreddit = `u/${user}/m/${custom_feed}` - let sortby = req.params.sort - let past = req.query.t - let before = req.query.before - let after = req.query.after - let api_req = req.query.api - let api_type = req.query.type - let api_target = req.query.target - - if(req.query.hasOwnProperty('api')) - api_req = true - else - api_req = false - - let d = `&after=${after}` - if(before) { - d = `&before=${before}` - } - - if(!sortby) { - sortby = 'hot' - } - - if(!['new', 'rising', 'controversial', 'top', 'gilded', 'hot'].includes(sortby)) { - console.log(`Got invalid sort.`, req.originalUrl) - return res.redirect(`/u/${user}`) - } - - if(past) { - if(sortby === 'controversial' || sortby === 'top') { - if(!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { - console.error(`Got invalid past.`, req.originalUrl) - return res.redirect(`/u/${user}/${sortby}`) - } - } else { - past = undefined - } - } else { - if(sortby === 'controversial' || sortby === 'top') { - past = 'day' - } - } - - let key = `${user.toLowerCase()}:m:${custom_feed}:${after}:${before}:sort:${sortby}:past:${past}` - redis.get(key, (error, json) => { - if(error) { - console.error(`Error getting the ${user} custom_feed key from redis.`, error) - return res.render('index', { json: null, user_preferences: req.cookies }) - } - if(json) { - console.log(`Got /u/${user} custom_feed key from redis.`); - (async () => { - if(api_req) { - return handleTedditApiSubreddit(json, req, res, 'redis', api_type, api_target, subreddit) - } else { - let processed_json = await processJsonSubreddit(json, 'redis', null, req.cookies) - if(!processed_json.error) { - return res.render('subreddit', { - json: processed_json, - subreddit: '../' + subreddit, - subreddit_about: null, - subreddit_front: (!before && !after) ? true : false, - sortby: sortby, - past: past, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled, - redis_key: key, - after: req.query.after, - before: req.query.before - }) - } else { - return res.render('subreddit', { - json: null, - error: true, - data: processed_json, - user_preferences: req.cookies - }) - } - } - })() - } else { - let url = '' - if(config.use_reddit_oauth) - url = `https://oauth.reddit.com/${subreddit}/${sortby}?api_type=json&count=25&g=GLOBAL&t=${past}${d}` - else - url = `https://reddit.com/${subreddit}/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${past}${d}` - fetch(encodeURI(url), redditApiGETHeaders()) - .then(result => { - if(result.status === 200) { - result.json() - .then(json => { - redis.setex(key, config.setexs.subreddit, JSON.stringify(json), (error) => { - if(error) { - console.error(`Error setting the ${subreddit} key to redis.`, error) - return res.render('subreddit', { json: null, user_preferences: req.cookies }) - } else { - console.log(`Fetched the JSON from reddit.com/r/${subreddit}.`); - (async () => { - if(api_req) { - return handleTedditApiSubreddit(json, req, res, 'from_online', api_type, api_target, subreddit) - } else { - let processed_json = await processJsonSubreddit(json, 'from_online', null, req.cookies) - return res.render('subreddit', { - json: processed_json, - subreddit: '../' + subreddit, - subreddit_about: null, - subreddit_front: (!before && !after) ? true : false, - sortby: sortby, - past: past, - user_preferences: req.cookies, - instance_nsfw_enabled: config.nsfw_enabled, - redis_key: key, - after: req.query.after, - before: req.query.before - }) - } - })() - } - }) - }) - } else { - if(result.status === 404) { - console.log('404 – Subreddit not found') - } else { - console.error(`Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}`) - console.error(config.reddit_api_error_text) - } - return res.render('index', { - json: null, - http_status_code: result.status, - user_preferences: req.cookies - }) - } - }).catch(error => { - console.error(`Error fetching the JSON file from reddit.com/${subreddit}.`, error) - }) - } - }) - }) - - /** - * POSTS - */ - - app.post('/saveprefs', (req, res, next) => { - let theme = req.body.theme - let flairs = req.body.flairs - let nsfw_enabled = req.body.nsfw_enabled - let highlight_controversial = req.body.highlight_controversial - let post_media_max_height = req.body.post_media_max_height - let collapse_child_comments = req.body.collapse_child_comments - let show_upvoted_percentage = req.body.show_upvoted_percentage - let domain_twitter = req.body.domain_twitter - let domain_youtube = req.body.domain_youtube - let domain_instagram = req.body.domain_instagram - - res.cookie('theme', theme, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(flairs === 'on') - flairs = 'true' - else - flairs = 'false' - res.cookie('flairs', flairs, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(nsfw_enabled === 'on') - nsfw_enabled = 'true' - else - nsfw_enabled = 'false' - res.cookie('nsfw_enabled', nsfw_enabled, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(highlight_controversial === 'on') - highlight_controversial = 'true' - else - highlight_controversial = 'false' - res.cookie('highlight_controversial', highlight_controversial, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(config.post_media_max_heights.hasOwnProperty(post_media_max_height) || !isNaN(post_media_max_height)) - res.cookie('post_media_max_height', post_media_max_height, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(collapse_child_comments === 'on') - collapse_child_comments = 'true' - else - collapse_child_comments = 'false' - res.cookie('collapse_child_comments', collapse_child_comments, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - if(show_upvoted_percentage === 'on') - show_upvoted_percentage = 'true' - else - show_upvoted_percentage = 'false' - res.cookie('show_upvoted_percentage', show_upvoted_percentage, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - res.cookie('domain_twitter', domain_twitter, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - res.cookie('domain_youtube', domain_youtube, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - res.cookie('domain_instagram', domain_instagram, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - - return res.redirect('/preferences') - }) - - app.post('/export_prefs', (req, res, next) => { - let export_saved = req.body.export_saved - let export_data = req.cookies - let export_to_file = req.body.export_to_file - - if(export_saved !== 'on') { - if(req.cookies.saved) - delete export_data.saved - } - - if(export_to_file === 'on') { - res.setHeader('Content-disposition', 'attachment; filename=teddit_prefs.json') - res.setHeader('Content-type', 'application/json') - return res.send(export_data) - } - - let r = `${(Math.random().toString(36)+'00000000000000000').slice(2, 10+2).toUpperCase()}` - let key = `prefs_key:${r}` - redis.set(key, JSON.stringify(export_data), (error) => { - if(error) { - console.error(`Error saving preferences to redis.`, error) - return res.redirect('/preferences') - } else { - return res.render('preferences', { user_preferences: req.cookies, instance_config: config, preferences_key: r }) - } - }) - }) - - app.post('/import_prefs', (req, res, next) => { - let body = '' - req.on('data', chunk => { - body += chunk.toString() - }) - req.on('end', () => { - body = body.toString() - try { - let json = body.split('Content-Type: application/json')[1].trim().split('--')[0] - let prefs = JSON.parse(json) - resetPreferences(res) - for(var setting in prefs) { - if(prefs.hasOwnProperty(setting)) { - res.cookie(setting, prefs[setting], { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true }) - } - } - return res.redirect('/preferences') - } catch(e) { - console.error(`Error importing preferences from a JSON file. Please report this error on https://codeberg.org/teddit/teddit.`, e) - } - }) - }) - - app.post('/r/:subreddit/comments/:id/:snippet', (req, res, next) => { - /** - * This is the "morechildren" route. This route is called when the - * "load more comments" button at the bottom of some post is clicked. - */ - if(!config.use_reddit_oauth) - return res.send(`This instance is using Reddit's public API (non-OAuth), and therefore this endpoint is not supported. In other words, this feature is only available if the instance is using Reddit OAuth API.`) - - let subreddit = req.params.subreddit - let id = req.params.id - let snippet = encodeURIComponent(req.params.snippet) - let post_url = `/r/${subreddit}/comments/${id}/${snippet}/` - let page = req.query.page - let comment_ids = req.body.comment_ids - - return res.redirect(`${post_url}?comment_ids=${comment_ids}&page=1`) - }) - - function resetPreferences(res) { - res.clearCookie('theme') - res.clearCookie('flairs') - res.clearCookie('nsfw_enabled') - res.clearCookie('highlight_controversial') - res.clearCookie('post_media_max_height') - res.clearCookie('collapse_child_comments') - res.clearCookie('show_upvoted_percentage') - res.clearCookie('subbed_subreddits') - res.clearCookie('domain_twitter') - res.clearCookie('domain_youtube') - res.clearCookie('domain_instagram') - } -} - diff --git a/routes/gallery.js b/routes/gallery.js new file mode 100644 index 00000000..3590aca0 --- /dev/null +++ b/routes/gallery.js @@ -0,0 +1,7 @@ +const galleryRoute = require('express').Router(); + +galleryRoute.get('/gallery/:id', (req, res, next) => { + return res.redirect(`/comments/${req.params.id}`); +}); + +module.exports = galleryRoute; diff --git a/routes/home.js b/routes/home.js new file mode 100644 index 00000000..67510652 --- /dev/null +++ b/routes/home.js @@ -0,0 +1,268 @@ +const config = require('../config'); +const { redis, fetch } = require('../app'); +const homeRoute = require('express').Router(); + +const processJsonSubreddit = require('../inc/processJsonSubreddit.js'); +const tedditApiSubreddit = require('../inc/teddit_api/handleSubreddit.js')(); +const processMoreComments = require('../inc/processMoreComments.js')(); + +homeRoute.get('/', (req, res, next) => { + if ( + (config.clean_homepage && req.cookies.prefer_frontpage !== 'true') || + (!config.clean_homepage && req.cookies.prefer_frontpage == 'undefined') + ) { + return res.render('homepage', { + user_preferences: req.cookies, + instance_config: config, + }); + } + + next(); +}); + +homeRoute.get([`/:sort?`, '/frontpage'], async (req, res, next) => { + let past = req.query.t; + let before = req.query.before; + let after = req.query.after; + let sortby = req.params.sort || ''; + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + + let proxyable = + sortby.includes('.jpg') || + sortby.includes('.png') || + sortby.includes('.jpeg') || + sortby.includes('.mp4') || + sortby.includes('.gif') || + sortby.includes('.gifv') + ? true + : false; + if (proxyable) { + let media_url = ''; + const replacable_media_domains = ['i.redd.it', 'v.redd.it', 'external-preview.redd.it', 'preview.redd.it'] + if (req.query.teddit_proxy) { + if (replacable_media_domains.includes(req.query.teddit_proxy)) { + let full_url = req.protocol + '://' + req.get('host') + req.originalUrl; + let u = new URL(full_url); + let filename = u.pathname || ''; + let query = u.search || ''; + if (query != '') { + let params = new URLSearchParams(query); + params.delete('teddit_proxy'); + query = '?' + params.toString(); + } + media_url = `https://${req.query.teddit_proxy}${filename}${query}`; + } + } else { + let params = new URLSearchParams(req.query).toString(); + media_url = `https://preview.redd.it/${sortby}?${params}`; + if (media_url.includes('teddit_proxy')) { + // if the URL includes teddit_proxy query param, remove everything after it + media_url = media_url.split('%3Fteddit_proxy')[0]; + } + } + + let proxied_media = await downloadAndSave(media_url); + if (proxied_media) { + return res.redirect(proxied_media); + } else { + return res.redirect('/'); + } + } + + let is_comment = + (sortby.length == 6 || sortby.length == 7) && + sortby != "rising" + ? true + : false; + + if (is_comment) { + return res.redirect('/comments/' + sortby); + } + + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + + if (sortby == '' || sortby == 'frontpage') { + sortby = 'hot'; + } + + if ( + [ + 'apple-touch-icon.png', + 'apple-touch-icon-precomposed.png', + 'apple-touch-icon-120x120.png', + 'apple-touch-icon-120x120-precomposed.png', + ].includes(sortby) + ) { + return res.sendStatus(404); // return 404 on shitty apple favicon stuff + } + + if ( + !['new', 'rising', 'controversial', 'top', 'gilded', 'hot'].includes(sortby) + ) { + console.log(`Got invalid sort.`, req.originalUrl); + return res.redirect('/'); + } + + if (past) { + if (sortby === 'controversial' || sortby === 'top') { + if (!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { + console.error(`Got invalid past.`, req.originalUrl); + return res.redirect(`/`); + } + } else { + past = undefined; + } + } else { + if (sortby === 'controversial' || sortby === 'top') { + past = 'day'; + } + } + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let raw_json = api_req && req.query.raw_json == '1' ? 1 : 0; + + let key = `/after:${after}:before:${before}:sort:${sortby}:past:${past}:raw_json:${raw_json}`; + + let subbed_subreddits = req.cookies.subbed_subreddits; + let get_subbed_subreddits = false; + if (subbed_subreddits && Array.isArray(subbed_subreddits)) { + get_subbed_subreddits = true; + subbed_subreddits = subbed_subreddits.join('+'); + key = `${subbed_subreddits.toLowerCase()}:${after}:${before}:sort:${sortby}:past:${past}:raw_json:${raw_json}`; + } + + redis.get(key, (error, json) => { + if (error) { + console.error('Error getting the frontpage key from redis.', error); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log('Got frontpage key from redis.'); + (async () => { + if (api_req) { + return handleTedditApiSubreddit( + json, + req, + res, + 'redis', + api_type, + api_target, + '/', + 'full' + ); + } else { + let processed_json = await processJsonSubreddit( + json, + 'redis', + null, + req.cookies + ); + return res.render('frontpage', { + json: processed_json, + sortby: sortby, + past: past, + user_preferences: req.cookies, + redis_key: key, + instance_config: config, + }); + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) { + if (get_subbed_subreddits) + url = `https://oauth.reddit.com/r/${subbed_subreddits}/${sortby}?api_type=json&count=25&g=GLOBAL&t=${past}${d}&raw_json=${raw_json}`; + else + url = `https://oauth.reddit.com/${sortby}?api_type=json&g=GLOBAL&t=${past}${d}&raw_json=${raw_json}`; + } else { + if (get_subbed_subreddits) + url = `https://reddit.com/r/${subbed_subreddits}/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${past}${d}&raw_json=${raw_json}`; + else + url = `https://reddit.com/${sortby}.json?g=GLOBAL&t=${past}${d}&raw_json=${raw_json}`; + } + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + key, + config.setexs.frontpage, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + 'Error setting the frontpage key to redis.', + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log('Fetched the frontpage from Reddit.'); + (async () => { + if (api_req) { + return handleTedditApiSubreddit( + json, + req, + res, + 'from_online', + api_type, + api_target, + '/', + 'full' + ); + } else { + let processed_json = await processJsonSubreddit( + json, + 'from_online', + null, + req.cookies + ); + return res.render('frontpage', { + json: processed_json, + sortby: sortby, + past: past, + user_preferences: req.cookies, + redis_key: key, + instance_config: config, + }); + } + })(); + } + } + ); + }); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error('Error fetching the frontpage JSON file.', error); + }); + } + }); +}); + +module.exports = homeRoute; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 00000000..8541395f --- /dev/null +++ b/routes/index.js @@ -0,0 +1,27 @@ +const galleryRoute = require('./gallery'); +const homeRoute = require('./home'); +const overridingRoutes = require('./overides'); +const pollRoute = require('./poll'); +const preferenceRoutes = require('./preferences'); +const saveRoutes = require('./save'); +const searchRoute = require('./search'); +const staticRoutes = require('./static'); +const subredditRoutes = require('./subreddit'); +const subscriptionRoutes = require('./subscription'); +const userRoutes = require('./user'); + +const allRoutes = require('express').Router(); + +allRoutes.use(overridingRoutes); +allRoutes.use(staticRoutes); +allRoutes.use(preferenceRoutes); +allRoutes.use(subredditRoutes); +allRoutes.use(userRoutes); +allRoutes.use(subscriptionRoutes); +allRoutes.use(saveRoutes); +allRoutes.use(searchRoute); +allRoutes.use(homeRoute); +allRoutes.use(galleryRoute); +allRoutes.use(pollRoute); + +module.exports = allRoutes; diff --git a/routes/overides.js b/routes/overides.js new file mode 100644 index 00000000..1822918a --- /dev/null +++ b/routes/overides.js @@ -0,0 +1,206 @@ +const config = require('../config'); +const overridingRoutes = require('express').Router(); + +overridingRoutes.all('*', (req, res, next) => { + let themeOverride = req.query.theme; + if (themeOverride) { + // Convert Dark to dark since the stylesheet has it lower case + themeOverride = themeOverride.toLowerCase(); + // This override here will set it for the current request + req.cookies.theme = themeOverride; + // this will set it for future requests + res.cookie('theme', themeOverride, { maxAge: 31536000, httpOnly: true }); + } else if (!req.cookies.theme && req.cookies.theme !== '') { + req.cookies.theme = config.theme; + } + + let flairsOverride = req.query.flairs; + if (flairsOverride) { + req.cookies.flairs = flairsOverride; + res.cookie('flairs', flairsOverride, { maxAge: 31536000, httpOnly: true }); + } + + let nsfwEnabledOverride = req.query.nsfw_enabled; + if (nsfwEnabledOverride) { + req.cookies.nsfw_enabled = nsfwEnabledOverride; + res.cookie('nsfw_enabled', nsfwEnabledOverride, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let highlightControversialOverride = req.query.highlight_controversial; + if (highlightControversialOverride) { + req.cookies.highlight_controversial = highlightControversialOverride; + res.cookie('highlight_controversial', highlightControversialOverride, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let postMediaMaxHeight = req.query.post_media_max_height; + if (postMediaMaxHeight) { + if ( + config.post_media_max_heights.hasOwnProperty(postMediaMaxHeight) || + !isNaN(postMediaMaxHeight) + ) { + req.cookies.post_media_max_height = postMediaMaxHeight; + res.cookie('post_media_max_height', postMediaMaxHeight, { + maxAge: 31536000, + httpOnly: true, + }); + } + } + + let collapseChildComments = req.query.collapse_child_comments; + if (collapseChildComments) { + req.cookies.collapse_child_comments = collapseChildComments; + res.cookie('collapse_child_comments', collapseChildComments, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let showUpvotedPercentage = req.query.show_upvoted_percentage; + if (showUpvotedPercentage) { + req.cookies.show_upvoted_percentage = showUpvotedPercentage; + res.cookie('show_upvoted_percentage', showUpvotedPercentage, { + maxAge: 31536000, + httpOnly: true, + }); + } else if (!req.cookies.show_upvoted_percentage) { + if (config.show_upvoted_percentage) { + req.cookies.show_upvoted_percentage = 'true'; + } + } + + let domainTwitter = req.query.domain_twitter; + if (domainTwitter) { + req.cookies.domain_twitter = domainTwitter; + res.cookie('domain_twitter', domainTwitter, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let domainYoutube = req.query.domain_youtube; + if (domainYoutube) { + req.cookies.domain_youtube = domainYoutube; + res.cookie('domain_youtube', domainYoutube, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let domainInstagram = req.query.domain_instagram; + if (domainInstagram) { + req.cookies.domain_instagram = domainInstagram; + res.cookie('domain_instagram', domainInstagram, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let domainQuora = req.query.domain_quora; + if (domainQuora) { + req.cookies.domain_quora = domainQuora; + res.cookie('domain_quora', domainQuora, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let domainImgur = req.query.domain_imgur; + if (domainImgur) { + req.cookies.domain_imgur = domainImgur; + res.cookie('domain_imgur', domainImgur, { + maxAge: 31536000, + httpOnly: true, + }); + } + + let videosMuted = req.query.videos_muted; + if (videosMuted) { + req.cookies.videos_muted = videosMuted; + res.cookie('videos_muted', videosMuted, { + maxAge: 31536000, + httpOnly: true, + }); + } + + if (!config.rate_limiting) { + return next(); + } + + const valid_reddit_starts = [ + '/https://old.reddit.com', + '/https://reddit.com', + '/https://www.reddit.com', + '/old.reddit.com', + '/reddit.com', + '/www.reddit.com', + ]; + for (var i = 0; i < valid_reddit_starts.length; i++) { + if (req.url.startsWith(valid_reddit_starts[i])) { + req.url = req.url.substring(1); + const redditRegex = /([A-z.]+\.)?(reddit(\.com))/gm; + let teddified_url = req.url.replace(redditRegex, ''); + if (teddified_url.includes('://')) { + teddified_url = teddified_url.split('://')[1]; + } + if (teddified_url == '') { + teddified_url = '/'; + } + return res.redirect(teddified_url); + } + } + + if (config.rate_limiting.enabled) { + /** + * This route enforces request limits based on an IP address if + * config.rate_limiting.enabled is true. By default it's false. + */ + + let ip = String( + req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + 'unknown' + ); + + if (ip === 'unknown') { + return next(); + } + + if (ratelimit_counts[ip] == undefined) { + ratelimit_counts[ip] = 0; + } + + if (ratelimit_timestamps[ip] == undefined) { + ratelimit_timestamps[ip] = Date.now(); + } + + let diff = Date.now() - ratelimit_timestamps[ip]; + let credit = (diff / 60000) * config.rate_limiting.limit_after_limited; + ratelimit_counts[ip] -= credit; + + if (ratelimit_counts[ip] < 0) { + ratelimit_counts[ip] = 0; + } + + ratelimit_counts[ip]++; + ratelimit_timestamps[ip] = Date.now(); + + if (ratelimit_counts[ip] > config.rate_limiting.initial_limit) { + console.log(`RATE LIMITED IP ADDRESS: ${ip}`); + return res.send( + `Hold your horses! You have hit the request limit. You should be able to refresh this page in a couple of seconds. If you think you are wrongfully limited, create an issue at https://codeberg.org/teddit/teddit. Rate limiting is highly experimental feature.` + ); + } else { + return next(); + } + } else { + return next(); + } +}); + +module.exports = overridingRoutes; diff --git a/routes/poll.js b/routes/poll.js new file mode 100644 index 00000000..83c4dab2 --- /dev/null +++ b/routes/poll.js @@ -0,0 +1,7 @@ +const pollRoute = require('express').Router(); + +pollRoute.get('/poll/:id', (req, res, next) => { + return res.redirect(`/comments/${req.params.id}`); +}); + +module.exports = pollRoute; diff --git a/routes/preferences.js b/routes/preferences.js new file mode 100644 index 00000000..ebe2911e --- /dev/null +++ b/routes/preferences.js @@ -0,0 +1,278 @@ +const config = require('../config'); +const { redis } = require('../app'); +const preferenceRoutes = require('express').Router(); + +function resetPreferences(res) { + res.clearCookie('theme'); + res.clearCookie('flairs'); + res.clearCookie('nsfw_enabled'); + res.clearCookie('highlight_controversial'); + res.clearCookie('post_media_max_height'); + res.clearCookie('collapse_child_comments'); + res.clearCookie('show_upvoted_percentage'); + res.clearCookie('show_upvotes') + res.clearCookie('subbed_subreddits'); + res.clearCookie('domain_twitter'); + res.clearCookie('domain_youtube'); + res.clearCookie('domain_instagram'); + res.clearCookie('domain_quora'); + res.clearCookie('domain_imgur'); + res.clearCookie('videos_muted'); + res.clearCookie('prefer_frontpage'); + res.clearCookie('show_large_gallery_images'); + res.clearCookie('default_comment_sort'); +} + +preferenceRoutes.get('/preferences', (req, res, next) => { + return res.render('preferences', { + user_preferences: req.cookies, + instance_config: config, + comment_sort_values: ['best', 'top', 'new', 'controversial', 'old', 'qa'], + }); +}); + +preferenceRoutes.get('/resetprefs', (req, res, next) => { + resetPreferences(res); + return res.redirect('/preferences'); +}); + +preferenceRoutes.get('/import_prefs/:key', (req, res, next) => { + let key = req.params.key; + if (!key) return res.redirect('/'); + if (key.length !== 10) return res.redirect('/'); + + key = `prefs_key:${key}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + `Error getting the preferences import key ${key} from redis.`, + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + try { + let prefs = JSON.parse(json); + let subbed_subreddits_is_set = false; + for (var setting in prefs) { + if (prefs.hasOwnProperty(setting)) { + res.cookie(setting, prefs[setting], { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + if (setting === 'subbed_subreddits') + subbed_subreddits_is_set = true; + } + } + if (!subbed_subreddits_is_set) res.clearCookie('subbed_subreddits'); + return res.redirect('/'); + } catch (e) { + console.error( + `Error setting imported preferences to the cookies. Key: ${key}.`, + error + ); + } + } else { + return res.redirect('/preferences'); + } + }); +}); + +preferenceRoutes.post('/saveprefs', (req, res, next) => { + let theme = req.body.theme; + let flairs = req.body.flairs; + let nsfw_enabled = req.body.nsfw_enabled; + let highlight_controversial = req.body.highlight_controversial; + let post_media_max_height = req.body.post_media_max_height; + let collapse_child_comments = req.body.collapse_child_comments; + let show_upvoted_percentage = req.body.show_upvoted_percentage; + let show_upvotes = req.body.show_upvotes; + let domain_twitter = req.body.domain_twitter; + let domain_youtube = req.body.domain_youtube; + let domain_instagram = req.body.domain_instagram; + let domain_quora = req.body.domain_quora; + let domain_imgur = req.body.domain_imgur; + let videos_muted = req.body.videos_muted; + let prefer_frontpage = req.body.prefer_frontpage; + let show_large_gallery_images = req.body.show_large_gallery_images; + let default_comment_sort = req.body.default_comment_sort; + + res.cookie('theme', theme, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (flairs === 'on') flairs = 'true'; + else flairs = 'false'; + res.cookie('flairs', flairs, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (nsfw_enabled === 'on') nsfw_enabled = 'true'; + else nsfw_enabled = 'false'; + res.cookie('nsfw_enabled', nsfw_enabled, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (highlight_controversial === 'on') highlight_controversial = 'true'; + else highlight_controversial = 'false'; + res.cookie('highlight_controversial', highlight_controversial, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if ( + config.post_media_max_heights.hasOwnProperty(post_media_max_height) || + !isNaN(post_media_max_height) + ) + res.cookie('post_media_max_height', post_media_max_height, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (collapse_child_comments === 'on') collapse_child_comments = 'true'; + else collapse_child_comments = 'false'; + res.cookie('collapse_child_comments', collapse_child_comments, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (show_upvoted_percentage === 'on') show_upvoted_percentage = 'true'; + else show_upvoted_percentage = 'false'; + res.cookie('show_upvoted_percentage', show_upvoted_percentage, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (show_upvotes === 'on') show_upvotes = 'true'; + else show_upvotes = 'false'; + res.cookie('show_upvotes', show_upvotes, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (videos_muted === 'on') videos_muted = 'true'; + else videos_muted = 'false'; + res.cookie('videos_muted', videos_muted, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + res.cookie('domain_twitter', domain_twitter, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + res.cookie('domain_youtube', domain_youtube, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + res.cookie('domain_instagram', domain_instagram, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + res.cookie('domain_quora', domain_quora, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + res.cookie('domain_imgur', domain_imgur, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (prefer_frontpage === 'on') prefer_frontpage = 'true'; + else prefer_frontpage = 'false'; + res.cookie('prefer_frontpage', prefer_frontpage, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (show_large_gallery_images === 'on') show_large_gallery_images = 'true'; + else show_large_gallery_images = 'false'; + res.cookie('show_large_gallery_images', show_large_gallery_images, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + res.cookie('default_comment_sort', default_comment_sort, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + return res.redirect('/preferences'); +}); + +preferenceRoutes.post('/export_prefs', (req, res, next) => { + let export_saved = req.body.export_saved; + let export_data = req.cookies; + let export_to_file = req.body.export_to_file; + + if (export_saved !== 'on') { + if (req.cookies.saved) delete export_data.saved; + } + + if (export_to_file === 'on') { + res.setHeader( + 'Content-disposition', + 'attachment; filename=teddit_prefs.json' + ); + res.setHeader('Content-type', 'application/json'); + return res.send(export_data); + } + + let r = `${(Math.random().toString(36) + '00000000000000000') + .slice(2, 10 + 2) + .toUpperCase()}`; + let key = `prefs_key:${r}`; + redis.set(key, JSON.stringify(export_data), (error) => { + if (error) { + console.error(`Error saving preferences to redis.`, error); + return res.redirect('/preferences'); + } else { + return res.render('preferences', { + user_preferences: req.cookies, + instance_config: config, + preferences_key: r, + }); + } + }); +}); + +preferenceRoutes.post('/import_prefs', (req, res, next) => { + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + req.on('end', () => { + body = body.toString(); + try { + let json = body + .split('Content-Type: application/json')[1] + .trim() + .split('--')[0]; + let prefs = JSON.parse(json); + resetPreferences(res); + for (var setting in prefs) { + if (prefs.hasOwnProperty(setting)) { + res.cookie(setting, prefs[setting], { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + } + } + return res.redirect('/preferences'); + } catch (e) { + console.error( + `Error importing preferences from a JSON file. Please report this error on https://codeberg.org/teddit/teddit.`, + e + ); + } + }); +}); + +module.exports = preferenceRoutes; diff --git a/routes/save.js b/routes/save.js new file mode 100644 index 00000000..2840b506 --- /dev/null +++ b/routes/save.js @@ -0,0 +1,314 @@ +const config = require('../config'); +const { redis, fetch } = require('../app'); +const saveRoutes = require('express').Router(); + +const processJsonSubreddit = require('../inc/processJsonSubreddit.js'); +const tedditApiSubreddit = require('../inc/teddit_api/handleSubreddit.js')(); +const processMoreComments = require('../inc/processMoreComments.js')(); + +saveRoutes.get('/saved', (req, res, next) => { + let saved = req.cookies.saved; + + if (!saved || !Array.isArray(saved)) { + return res.render('saved', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + + let key = `saved_posts:${saved.join(',')}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + `Error getting saved_post ${saved_post} key from redis.`, + error + ); + return res.redirect('/'); + } + if (json) { + (async () => { + let processed_json = await processJsonSubreddit( + json, + 'redis', + null, + req.cookies, + true + ); + if (!processed_json.error) { + return res.render('saved', { + json: processed_json, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + return res.render('subreddit', { + json: null, + error: true, + data: processed_json, + user_preferences: req.cookies, + instance_config: config, + }); + } + })(); + } + }); +}); + +saveRoutes.get('/save/:id', (req, res, next) => { + let post_id = req.params.id; + let redis_key = req.query.rk; + let back = req.query.b; + let saved = req.cookies.saved; + let fetched = req.query.f; + + if (!post_id || !redis_key) return res.redirect('/saved'); + + if (!saved || !Array.isArray(saved)) saved = []; + + if (saved.length > 100) + return res.send('You can not save more than 100 posts.'); + + redis.get(redis_key, (error, json) => { + if (error) { + console.error( + `Error getting the ${redis_key} key from redis (via /save/).`, + error + ); + return res.redirect('/'); + } + if (json) { + json = JSON.parse(json); + if (fetched === 'true' || redis_key.includes('/comments/')) + json = json[0]; + + let post_to_save = false; + for (var i = 0; i < json.data.children.length; i++) { + let post = json.data.children[i]; + if (post.data.id === post_id) { + post_to_save = post; + break; + } + } + + if (post_to_save) { + if (!saved || !Array.isArray(saved)) saved = []; + + for (var i = 0; i < saved.length; i++) { + if (post_to_save.data.id === saved[i]) return res.redirect('/saved'); + } + + let key = `saved_posts:${saved.join(',')}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + `Error getting saved_posts ${key} key from redis.`, + error + ); + return res.redirect('/'); + } + links = JSON.parse(json); + if (!links) links = []; + + links.unshift(post_to_save); + saved.unshift(post_to_save.data.id); + res.cookie('saved', saved, { + maxAge: 3 * 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + let new_key = `saved_posts:${saved.join(',')}`; + redis.set(new_key, JSON.stringify(links), (error) => { + if (error) + console.error(`Error saving ${new_key} to redis.`, error); + + if (!back) return res.redirect('/saved'); + else { + back = back.replace(/§2/g, '?').replace(/§1/g, '&'); + return res.redirect(back); + } + }); + }); + } else { + return res.redirect(`/comments/${post_id}/?save=true&b=${back}`); + } + } else { + return res.redirect(`/comments/${post_id}/?save=true&b=${back}`); + } + }); +}); + +saveRoutes.get('/unsave/:id', (req, res, next) => { + let post_id = req.params.id; + let back = req.query.b; + let saved = req.cookies.saved; + + if (!post_id) return res.redirect('/saved'); + + if (!saved || !Array.isArray(saved)) return res.redirect('/saved'); + + let key = `saved_posts:${saved.join(',')}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + `Error getting the ${key} key from redis (via /save/).`, + error + ); + return res.redirect('/'); + } + if (json) { + json = JSON.parse(json); + let post_found = false; + for (var i = 0; i < json.length; i++) { + if (json[i].data.id === post_id) { + post_found = true; + json.splice(i, 1); + for (var j = 0; j < saved.length; j++) { + if (saved[j] === post_id) saved.splice(j, 1); + } + } + } + if (post_found) { + res.cookie('saved', saved, { + maxAge: 3 * 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + let new_key = `saved_posts:${saved.join(',')}`; + redis.set(new_key, JSON.stringify(json), (error) => { + if (error) console.error(`Error saving ${new_key} to redis.`, error); + + if (!back) return res.redirect('/saved'); + else { + back = back.replace(/§2/g, '?').replace(/§1/g, '&'); + return res.redirect(back); + } + }); + } else { + return res.redirect(`/saved`); + } + } else { + return res.redirect(`/saved`); + } + }); +}); + +saveRoutes.get( + '/comments/:post_id/:comment?/:comment_id?', + (req, res, next) => { + let post_id = req.params.post_id; + let comment = req.params.comment; + let comment_id = req.params.comment_id; + let back = req.query.b; + let save = req.query.save; + let post_url = false; + let comment_url = false; + + if (comment) + if (comment !== 'comment' || !comment_id) return res.redirect('/'); + + if (comment) comment_url = true; + else post_url = true; + + let key = `/shorturl:post:${post_id}:comment:${comment_id}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + 'Error getting the short URL for post key from redis.', + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log('Got short URL for post key from redis.'); + json = JSON.parse(json); + if (post_url) { + if (save === 'true') + return res.redirect(`/save/${post_id}/?rk=${key}&b=${back}&f=true`); + return res.redirect(json[0].data.children[0].data.permalink); + } else { + return res.redirect(json[1].data.children[0].data.permalink); + } + } else { + let url = ''; + if (config.use_reddit_oauth) { + if (post_url) + url = `https://oauth.reddit.com/comments/${post_id}?api_type=json`; + else + url = `https://oauth.reddit.com/comments/${post_id}/comment/${comment_id}?api_type=json`; + } else { + if (post_url) + url = `https://reddit.com/comments/${post_id}.json?api_type=json`; + else + url = `https://reddit.com/comments/${post_id}/comment/${comment_id}.json?api_type=json`; + } + + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + key, + config.setexs.shorts, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + 'Error setting the short URL for post key to redis.', + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + 'Fetched the short URL for post from Reddit.' + ); + if (post_url) { + if (save === 'true') + return res.redirect( + `/save/${post_id}/?rk=${key}&b=${back}&f=true` + ); + return res.redirect( + json[0].data.children[0].data.permalink + ); + } else { + return res.redirect( + json[1].data.children[0].data.permalink + ); + } + } + } + ); + }); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + 'Error fetching the short URL for post with sortby JSON file.', + error + ); + }); + } + }); + } +); + +module.exports = saveRoutes; diff --git a/routes/search.js b/routes/search.js new file mode 100644 index 00000000..e795689c --- /dev/null +++ b/routes/search.js @@ -0,0 +1,50 @@ +const config = require('../config'); +const searchRoute = require('express').Router(); + +searchRoute.get('/search', (req, res, next) => { + let q = req.query.q; + + if (typeof q === 'undefined') { + return res.render('search', { + json: { posts: [] }, + no_query: true, + q: '', + restrict_sr: undefined, + nsfw: undefined, + subreddit: 'all', + sortby: undefined, + past: undefined, + user_preferences: req.cookies, + instance_config: config, + }); + } + + let restrict_sr = req.query.restrict_sr; + let nsfw = req.query.nsfw; + let sortby = req.query.sort; + let past = req.query.t; + let after = req.query.after; + let before = req.query.before; + if (!after) { + after = ''; + } + if (!before) { + before = ''; + } + if (restrict_sr !== 'on') { + restrict_sr = 'off'; + } + + if (nsfw !== 'on') { + nsfw = 'off'; + } + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + return res.redirect( + `/r/all/search?q=${q}&restrict_sr=${restrict_sr}&nsfw=${nsfw}&sort=${sortby}&t=${past}${d}` + ); +}); + +module.exports = searchRoute; diff --git a/routes/static.js b/routes/static.js new file mode 100644 index 00000000..43b22904 --- /dev/null +++ b/routes/static.js @@ -0,0 +1,13 @@ +const config = require('../config'); + +const staticRoutes = require('express').Router(); + +staticRoutes.get('/privacy', (req, res, next) => { + return res.render('privacypolicy', { user_preferences: req.cookies, instance_config: config }); +}); + +staticRoutes.get('/about', (req, res, next) => { + return res.render('about', { user_preferences: req.cookies, instance_config: config }); +}); + +module.exports = staticRoutes; diff --git a/routes/subreddit.js b/routes/subreddit.js new file mode 100644 index 00000000..82139fc3 --- /dev/null +++ b/routes/subreddit.js @@ -0,0 +1,1202 @@ +const config = require('../config'); +const { redis, fetch, RedditAPI } = require('../app'); +const subredditRoutes = require('express').Router(); + +const { + processJsonPost, + finalizeJsonPost, +} = require('../inc/processJsonPost.js'); +const { + processSubredditAbout +} = require('../inc/processSubredditAbout.js'); +const processSearchResults = require('../inc/processSearchResults.js'); +const processJsonSubreddit = require('../inc/processJsonSubreddit.js'); +const tedditApiSubreddit = require('../inc/teddit_api/handleSubreddit.js')(); +const tedditApiPost = require('../inc/teddit_api/handlePost.js')(); +const processMoreComments = require('../inc/processMoreComments.js'); +const processJsonSubredditsExplore = require('../inc/processSubredditsExplore.js'); + +subredditRoutes.get('/r/:subreddit/search', (req, res, next) => { + let subreddit = req.params.subreddit; + let q = req.query.q; + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + let api_mode = req.query.mode; + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let raw_json = api_req && req.query.raw_json == '1' ? 1 : 0; + + if (typeof q === 'undefined') { + return res.render('search', { + json: { posts: [] }, + no_query: true, + q: '', + restrict_sr: undefined, + nsfw: undefined, + subreddit: subreddit, + sortby: undefined, + past: undefined, + user_preferences: req.cookies, + instance_config: config, + }); + } + + let restrict_sr = req.query.restrict_sr; + let nsfw = req.query.nsfw; + let sortby = req.query.sort; + let past = req.query.t; + let after = req.query.after; + let before = req.query.before; + if (!after) { + after = ''; + } + if (!before) { + before = ''; + } + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + + if (restrict_sr !== 'on') { + restrict_sr = 'off'; + } + + if (nsfw !== 'on') { + nsfw = 'off'; + } + + let count = '&count=25'; + if (after == '') { + count = ''; + } + + let key = `search:${subreddit}:${q}:${restrict_sr}:${sortby}:${past}:${after}:${before}:${nsfw}:raw_json:${raw_json}`; + redis.get(key, (error, json) => { + if (error) { + console.error('Error getting the search key from redis.', error); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log('Got search key from redis.'); + (async () => { + if (api_req) { + return handleTedditApiSubredditSearch( + json, + req, + res, + 'redis', + api_type, + api_target, + subreddit, + q, + api_mode + ); + } else { + let processed_json = await processSearchResults( + json, + false, + after, + before, + req.cookies + ); + return res.render('search', { + json: processed_json, + no_query: false, + q: q, + restrict_sr: restrict_sr, + nsfw: nsfw, + subreddit: subreddit, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_config: config, + }); + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/r/${subreddit}/search?api_type=json&q=${q}&restrict_sr=${restrict_sr}&include_over_18=${nsfw}&sort=${sortby}&t=${past}${count}${d}&raw_json=${raw_json}`; + else + url = `https://reddit.com/r/${subreddit}/search.json?api_type=json&q=${q}&restrict_sr=${restrict_sr}&include_over_18=${nsfw}&sort=${sortby}&t=${past}${count}${d}&raw_json=${raw_json}`; + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + (async () => { + /** + * Fetch suggested subreddits when the restrict_sr option is + * turned off ("limit my search to") and we are on the first search + * page (just like in Reddit). + */ + json.suggested_subreddits = {}; + if (restrict_sr === 'off' && before == '' && after == '') { + let url = `https://reddit.com/subreddits/search.json?q=${q}&include_over_18=${nsfw}&limit=3`; + const response = await fetch(encodeURI(url)); + const data = await response.json(); + json.suggested_subreddits = data; + } + + redis.setex( + key, + config.setexs.searches, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + 'Error setting the searches key to redis.', + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log('Fetched search results from Reddit.'); + (async () => { + if (api_req) { + return handleTedditApiSubredditSearch( + json, + req, + res, + 'from_online', + api_type, + api_target, + subreddit, + q, + api_mode + ); + } else { + let processed_json = await processSearchResults( + json, + true, + after, + before, + req.cookies + ); + return res.render('search', { + no_query: false, + json: processed_json, + q: q, + restrict_sr: restrict_sr, + nsfw: nsfw, + subreddit: subreddit, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_config: config, + }); + } + })(); + } + } + ); + })(); + }); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error('Error fetching the frontpage JSON file.', error); + }); + } + }); +}); + +subredditRoutes.get('/r/:subreddit/about', (req, res, next) => { + let subreddit = req.params.subreddit; + let api_type = req.query.type; + let api_target = req.query.target; + let api_mode = req.query.mode; + + if (!req.query.hasOwnProperty('api')) { + console.log(`This route is only available via the API.`, req.originalUrl); + return res.redirect(`/r/${subreddit}`); + } + + let raw_json = req.query.raw_json == '1' ? 1 : 0; + + let key = `about:${subreddit.toLowerCase()}:raw_json:${raw_json}`; + redis.get(key, (error, json) => { + if (error) { + console.error(`Error getting the about key from redis.`, error); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got about key from redis.`); + (async () => { + return handleTedditApiSubredditAbout( + json, + res, + 'redis', + api_target + ); + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/r/${subreddit}/about.json?api_type=json&raw_json=${raw_json}`; + else + url = `https://reddit.com/r/${subreddit}/about.json?api_type=json&raw_json=${raw_json}`; + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + key, + config.setexs.subreddit, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + `Error setting the about key to redis.`, + error + ); + return res.render('subreddit', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + `Fetched the JSON from reddit.com/r/${subreddit}/about.` + ); + (async () => { + return handleTedditApiSubredditAbout( + json, + res, + 'from_online', + api_target + ); + })(); + } + } + ); + }); + } else { + if (result.status === 404) { + console.log('404 – Subreddit not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com/r/${subreddit}/about.`, + error + ); + }); + } + }); +}); + +subredditRoutes.get( + '/r/:subreddit/wiki/:page?/:sub_page?', + (req, res, next) => { + let subreddit = req.params.subreddit; + let page = req.params.page; + let sub_page = req.params.sub_page || ''; + + if (!page) page = 'index'; + + if (sub_page != '') sub_page = `/${sub_page}`; + + function formatWikipagelisting(json, subreddit) { + let html = '
    '; + if (json.kind === 'wikipagelisting' && json.data) { + for (var i = 0; i < json.data.length; i++) { + let d = json.data[i]; + html += `
  • ${d}
  • `; + } + } + html += '
'; + return html; + } + + let key = `${subreddit.toLowerCase()}:wiki:page:${page}:sub_page:${sub_page}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + `Error getting the ${subreddit} wiki key from redis.`, + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got /r/${subreddit} wiki key from redis.`); + json = JSON.parse(json); + return res.render('subreddit_wiki', { + content_html: + page !== 'pages' + ? unescape(json.data.content_html) + : formatWikipagelisting(json, subreddit), + subreddit: subreddit, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/r/${subreddit}/wiki/${page}${sub_page}?api_type=json`; + else + url = `https://reddit.com/r/${subreddit}/wiki/${page}${sub_page}.json?api_type=json`; + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + key, + config.setexs.wikis, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + `Error setting the ${subreddit} wiki key to redis.`, + error + ); + return res.render('subreddit', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + `Fetched the JSON from reddit.com/r/${subreddit}/wiki.` + ); + return res.render('subreddit_wiki', { + content_html: + page !== 'pages' + ? unescape(json.data.content_html) + : formatWikipagelisting(json, subreddit), + subreddit: subreddit, + user_preferences: req.cookies, + instance_config: config, + }); + } + } + ); + }); + } else { + if (result.status === 404) { + console.log('404 – Subreddit wiki not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com/r/${subreddit}/wiki.`, + error + ); + }); + } + }); + } +); + +subredditRoutes.get('/r/:subreddit/w/:page?/:sub_page?', (req, res, next) => { + /* "w" is a shorturl for wikis for example https://old.reddit.com/r/privacytoolsIO/w/index */ + let subreddit = req.params.subreddit; + let page = req.params.page; + let sub_page = req.params.sub_page || ''; + + if (!page) page = 'index'; + + if (sub_page != '') sub_page = `/${sub_page}`; + + return res.redirect(`/r/${subreddit}/wiki/${page}${sub_page}`); +}); + +subredditRoutes.get('/r/random', (req, res, next) => { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/r/random?api_type=json&count=25&g=GLOBAL`; + else url = `https://reddit.com/r/random.json?api_type=json&count=25&g=GLOBAL`; + + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + let subreddit = json.data.children[0].data.subreddit; + if (subreddit) { + let key = `${subreddit.toLowerCase()}:undefined:undefined:sort:hot:past:undefined`; + redis.setex( + key, + config.setexs.subreddit, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + `Error setting the random subreddit key to redis.`, + error + ); + return res.render('subreddit', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + `Fetched the JSON from reddit.com/r/${subreddit}.` + ); + return res.redirect(`/r/${subreddit}`); + } + } + ); + } else { + console.error(`Fetching random subreddit failed.`, json); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + }); + } else { + if (result.status === 404) { + console.log('404 – Subreddit not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com/r/random.`, + error + ); + }); +}); + +subredditRoutes.get('/r/:subreddit/:sort?', (req, res, next) => { + let subreddit = req.params.subreddit; + let sortby = req.params.sort; + let past = req.query.t; + let before = req.query.before; + let after = req.query.after; + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + let api_mode = req.query.mode; + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let raw_json = api_req && req.query.raw_json == '1' ? 1 : 0; + + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + + if (!sortby) { + sortby = 'hot'; + } + + if ( + !['new', 'rising', 'controversial', 'top', 'gilded', 'hot'].includes(sortby) + ) { + console.log(`Got invalid sort.`, req.originalUrl); + return res.redirect(`/r/${subreddit}`); + } + + if (past) { + if (sortby === 'controversial' || sortby === 'top') { + if (!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { + console.error(`Got invalid past.`, req.originalUrl); + return res.redirect(`/r/${subreddit}/${sortby}`); + } + } else { + past = undefined; + } + } else { + if (sortby === 'controversial' || sortby === 'top') { + past = 'day'; + } + } + + let key = `${subreddit.toLowerCase()}:${after}:${before}:sort:${sortby}:past:${past}:raw_json:${raw_json}`; + redis.get(key, (error, json) => { + if (error) { + console.error(`Error getting the ${subreddit} key from redis.`, error); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got /r/${subreddit} key from redis.`); + (async () => { + if (api_req) { + return handleTedditApiSubreddit( + json, + req, + res, + 'redis', + api_type, + api_target, + subreddit, + api_mode + ); + } else { + let processed_json = await processJsonSubreddit( + json, + 'redis', + null, + req.cookies + ); + let subreddit_about = await processSubredditAbout( + subreddit, + redis, + fetch, + RedditAPI + ); + if (!processed_json.error) { + return res.render('subreddit', { + json: processed_json, + subreddit: subreddit, + subreddit_about: subreddit_about, + subreddit_front: !before && !after ? true : false, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + redis_key: key, + after: req.query.after, + before: req.query.before, + instance_config: config, + }); + } else { + return res.render('subreddit', { + json: null, + error: true, + data: processed_json, + user_preferences: req.cookies, + instance_config: config, + }); + } + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/r/${subreddit}/${sortby}?api_type=json&count=25&g=GLOBAL&t=${past}${d}&raw_json=${raw_json}`; + else + url = `https://reddit.com/r/${subreddit}/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${past}${d}&raw_json=${raw_json}`; + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + key, + config.setexs.subreddit, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + `Error setting the ${subreddit} key to redis.`, + error + ); + return res.render('subreddit', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + `Fetched the JSON from reddit.com/r/${subreddit}.` + ); + (async () => { + if (api_req) { + return handleTedditApiSubreddit( + json, + req, + res, + 'from_online', + api_type, + api_target, + subreddit, + api_mode + ); + } else { + let processed_json = await processJsonSubreddit( + json, + 'from_online', + null, + req.cookies + ); + let subreddit_about = await processSubredditAbout( + subreddit, + redis, + fetch, + RedditAPI + ); + return res.render('subreddit', { + json: processed_json, + subreddit: subreddit, + subreddit_about: subreddit_about, + subreddit_front: !before && !after ? true : false, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + redis_key: key, + after: req.query.after, + before: req.query.before, + instance_config: config, + }); + } + })(); + } + } + ); + }); + } else { + if (result.status === 404) { + console.log('404 – Subreddit not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com/r/${subreddit}.`, + error + ); + }); + } + }); +}); + +subredditRoutes.get( + '/r/:subreddit/comments/:id/:snippet?/:comment_id?', + (req, res, next) => { + let subreddit = req.params.subreddit; + let id = req.params.id; + let snippet = encodeURIComponent(req.params.snippet); + let sortby = req.query.sort || req.cookies.default_comment_sort; + let comment_id = ''; + let viewing_comment = false; + let comment_ids = req.query.comment_ids; + let context = parseInt(req.query.context); + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let raw_json = api_req && req.query.raw_json == '1' ? 1 : 0; + + if (req.params.comment_id) { + comment_id = `${req.params.comment_id}/`; + viewing_comment = true; + } + + if (sortby === 'best') { + // in Reddit the sorting "best" is the label, but the actual key "confidence" + sortby = 'confidence'; + } + + if (!sortby) { + sortby = config.post_comments_sort; + } + + if ( + ![ + 'confidence', + 'top', + 'new', + 'controversial', + 'old', + 'qa', + 'random', + ].includes(sortby) + ) { + console.log(`Got invalid sort.`, req.originalUrl); + return res.redirect('/'); + } + + let comments_url = `/r/${subreddit}/comments/${id}/${snippet}/${comment_id}`; + let post_url = `/r/${subreddit}/comments/${id}/${snippet}/`; + let comments_key = `${comments_url}:sort:${sortby}:raw_json:${raw_json}`; + + redis.get(comments_key, (error, json) => { + if (error) { + console.error( + `Error getting the ${comments_url} key from redis.`, + error + ); + return res.render('frontpage', { + post: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got ${comments_url} key from redis.`); + (async () => { + if (api_req) { + return handleTedditApiPost( + json, + req, + res, + 'redis', + api_type, + api_target + ); + } else { + let parsed = false; + let more_comments = null; + if (comment_ids) { + let key = `${post_url}:morechildren:comment_ids:${comment_ids}`; + more_comments = await processMoreComments( + fetch, + redis, + post_url, + comment_ids, + id + ); + + if (more_comments === false) { + return res.redirect(post_url); + } else { + json = JSON.parse(json); + json[1].data.children = more_comments; + parsed = true; + } + } + + let processed_json = await processJsonPost(json, parsed, req.cookies); + let finalized_json = await finalizeJsonPost( + processed_json, + id, + post_url, + more_comments, + viewing_comment, + req.cookies + ); + return res.render('post', { + post: finalized_json.post_data, + comments: finalized_json.comments, + viewing_comment: viewing_comment, + post_url: post_url, + subreddit: subreddit, + sortby: sortby, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + instance_videos_muted: config.videos_muted, + post_media_max_heights: config.post_media_max_heights, + redis_key: comments_key, + instance_config: config, + }); + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com${comments_url}?api_type=json&sort=${sortby}&context=${context}&raw_json=${raw_json}`; + else + url = `https://reddit.com${comments_url}.json?api_type=json&sort=${sortby}&context=${context}&raw_json=${raw_json}`; + + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + comments_key, + config.setexs.posts, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + `Error setting the ${comments_url} key to redis.`, + error + ); + return res.render('post', { + post: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + `Fetched the JSON from reddit.com${comments_url}.` + ); + (async () => { + if (api_req) { + return handleTedditApiPost( + json, + req, + res, + 'from_online', + api_type, + api_target + ); + } else { + let more_comments = null; + if (comment_ids) { + let key = `${post_url}:morechildren:comment_ids:${comment_ids}`; + more_comments = await processMoreComments( + fetch, + redis, + post_url, + comment_ids, + id + ); + + if (more_comments === false) { + return res.redirect(post_url); + } else { + json[1].data.children = more_comments; + } + } + + let processed_json = await processJsonPost( + json, + true, + req.cookies + ); + let finalized_json = await finalizeJsonPost( + processed_json, + id, + post_url, + more_comments, + viewing_comment, + req.cookies + ); + return res.render('post', { + post: finalized_json.post_data, + comments: finalized_json.comments, + viewing_comment: viewing_comment, + post_url: post_url, + subreddit: subreddit, + sortby: sortby, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + instance_videos_muted: config.videos_muted, + post_media_max_heights: config.post_media_max_heights, + redis_key: comments_key, + instance_config: config, + }); + } + })(); + } + } + ); + }); + } else { + if (result.status === 404) { + console.log('404 – Post not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + http_statustext: result.statusText, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com${comments_url}.`, + error + ); + }); + } + }); + } +); + +subredditRoutes.post( + '/r/:subreddit/comments/:id/:snippet', + (req, res, next) => { + /** + * This is the "morechildren" route. This route is called when the + * "load more comments" button at the bottom of some post is clicked. + */ + if (!config.use_reddit_oauth) + return res.send( + `This instance is using Reddit's public API (non-OAuth), and therefore this endpoint is not supported. In other words, this feature is only available if the instance is using Reddit OAuth API.` + ); + + let subreddit = req.params.subreddit; + let id = req.params.id; + let snippet = encodeURIComponent(req.params.snippet); + let post_url = `/r/${subreddit}/comments/${id}/${snippet}/`; + let page = req.query.page; + let comment_ids = req.body.comment_ids; + + return res.redirect(`${post_url}?comment_ids=${comment_ids}&page=1`); + } +); + +subredditRoutes.get('/subreddits/:sort?', (req, res, next) => { + let q = req.query.q; + let nsfw = req.query.nsfw; + let after = req.query.after; + let before = req.query.before; + let sortby = req.params.sort; + let searching = false; + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let raw_json = api_req && req.query.raw_json == '1' ? 1 : 0; + + if (!after) { + after = ''; + } + if (!before) { + before = ''; + } + + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + + if (nsfw !== 'on') { + nsfw = 'off'; + } + + if (!sortby) { + sortby = ''; + } + + let key = `subreddits:sort:${sortby}${d}:raw_json:${raw_json}`; + + if (sortby === 'search') { + if (typeof q == 'undefined' || q == '') return res.redirect('/subreddits'); + + key = `subreddits:search:q:${q}:nsfw:${nsfw}${d}:raw_json:${raw_json}`; + searching = true; + } + + redis.get(key, (error, json) => { + if (error) { + console.error(`Error getting the subreddits key from redis.`, error); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got subreddits key from redis.`); + (async () => { + if (api_req) { + return handleTedditApiSubredditsExplore( + json, + req, + res, + 'redis', + api_type, + api_target, + q + ); + } else { + let processed_json = await processJsonSubredditsExplore( + json, + 'redis', + null, + req.cookies + ); + if (!processed_json.error) { + return res.render('subreddits_explore', { + json: processed_json, + sortby: sortby, + after: after, + before: before, + q: q, + nsfw: nsfw, + searching: searching, + subreddits_front: !before && !after ? true : false, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + instance_config: config, + }); + } else { + return res.render('subreddits_explore', { + json: null, + error: true, + data: processed_json, + user_preferences: req.cookies, + instance_config: config, + }); + } + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) { + if (!searching) + url = `https://oauth.reddit.com/subreddits/${sortby}?api_type=json&count=25&g=GLOBAL&t=${d}&raw_json=${raw_json}`; + else + url = `https://oauth.reddit.com/subreddits/search?api_type=json&q=${q}&include_over_18=${nsfw}${d}&raw_json=${raw_json}`; + } else { + if (!searching) + url = `https://reddit.com/subreddits/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${d}&raw_json=${raw_json}`; + else + url = `https://reddit.com/subreddits/search.json?api_type=json&q=${q}&include_over_18=${nsfw}${d}&raw_json=${raw_json}`; + } + + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + let ex = config.setexs.subreddits_explore.front; + if (sortby === 'new') + ex = config.setexs.subreddits_explore.new_page; + redis.setex(key, ex, JSON.stringify(json), (error) => { + if (error) { + console.error( + `Error setting the subreddits key to redis.`, + error + ); + return res.render('subreddits_explore', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log(`Fetched the JSON from reddit.com/subreddits.`); + (async () => { + if (api_req) { + return handleTedditApiSubredditsExplore( + json, + req, + res, + 'from_online', + api_type, + api_target, + q + ); + } else { + let processed_json = await processJsonSubredditsExplore( + json, + 'from_online', + null, + req.cookies + ); + return res.render('subreddits_explore', { + json: processed_json, + sortby: sortby, + after: after, + before: before, + q: q, + nsfw: nsfw, + searching: searching, + subreddits_front: !before && !after ? true : false, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + instance_config: config, + }); + } + })(); + } + }); + }); + } else { + if (result.status === 404) { + console.log('404 – Subreddits not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com/subreddits.`, + error + ); + }); + } + }); +}); + +module.exports = subredditRoutes; diff --git a/routes/subscription.js b/routes/subscription.js new file mode 100644 index 00000000..deb8bf10 --- /dev/null +++ b/routes/subscription.js @@ -0,0 +1,82 @@ +const subscriptionRoutes = require('express').Router(); + +subscriptionRoutes.get('/subscribe/:subreddit', (req, res, next) => { + let subreddit = req.params.subreddit; + let subbed = req.cookies.subbed_subreddits; + let back = req.query.b; + + if (!subreddit) return res.redirect('/'); + + if (!subbed || !Array.isArray(subbed)) subbed = []; + + if (!subbed.includes(subreddit)) subbed.push(subreddit); + + res.cookie('subbed_subreddits', subbed, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (!back) return res.redirect('/r/' + subreddit); + else { + back = back.replace(/,/g, '+').replace(/§1/g, '&'); + return res.redirect(back); + } +}); + +subscriptionRoutes.get( + '/import_subscriptions/:subreddits', + (req, res, next) => { + let subreddits = req.params.subreddits; + let subbed = req.cookies.subbed_subreddits; + let back = req.query.b; + + if (!subreddits) return res.redirect('/'); + + if (!subbed || !Array.isArray(subbed)) subbed = []; + + subreddits = subreddits.split('+'); + for (var i = 0; i < subreddits.length; i++) { + if (!subbed.includes(subreddits[i])) subbed.push(subreddits[i]); + } + + res.cookie('subbed_subreddits', subbed, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (!back) return res.redirect('/r/' + subreddits); + else { + back = back.replace(/,/g, '+').replace(/ /g, '+'); + return res.redirect(back); + } + } +); + +subscriptionRoutes.get('/unsubscribe/:subreddit', (req, res, next) => { + let subreddit = req.params.subreddit; + let subbed = req.cookies.subbed_subreddits; + let back = req.query.b; + + if (!subreddit || !subbed || !Array.isArray(subbed)) { + res.clearCookie('subbed_subreddits'); + return res.redirect('/'); + } + + var index = subbed.indexOf(subreddit); + if (index !== -1) subbed.splice(index, 1); + + if (subbed.length <= 0) res.clearCookie('subbed_subreddits'); + else + res.cookie('subbed_subreddits', subbed, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + + if (!back) return res.redirect('/r/' + subreddit); + else { + back = back.replace(/,/g, '+').replace(/§1/g, '&'); + return res.redirect(back); + } +}); + +module.exports = subscriptionRoutes; diff --git a/routes/user.js b/routes/user.js new file mode 100644 index 00000000..4b7fb1cb --- /dev/null +++ b/routes/user.js @@ -0,0 +1,478 @@ +const config = require('../config'); +const { redis, fetch } = require('../app'); +const userRoutes = require('express').Router(); + +const processJsonUser = require('../inc/processJsonUser.js'); +const tedditApiUser = require('../inc/teddit_api/handleUser.js')(); +const processJsonSubreddit = require('../inc/processJsonSubreddit.js'); +const tedditApiSubreddit = require('../inc/teddit_api/handleSubreddit.js')(); +const processMoreComments = require('../inc/processMoreComments.js')(); + +userRoutes.get('/user/:user/:kind?', (req, res, next) => { + let kind = ''; + if (req.params.kind) kind = `/${req.params.kind}`; + let q = ''; + if (req.query.sort) q += `?sort=${req.query.sort}&`; + if (req.query.t) q += `t=${req.query.t}`; + + res.redirect(`/u/${req.params.user}${kind}${q}`); +}); + +userRoutes.get('/u/:user/:kind?', (req, res, next) => { + let user = req.params.user; + let after = req.query.after; + let before = req.query.before; + let post_type = req.params.kind; + let kind = post_type; + let user_data = {}; + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let raw_json = api_req && req.query.raw_json == '1' ? 1 : 0; + + if (!after) { + after = ''; + } + if (!before) { + before = ''; + } + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + + post_type = `/${post_type}`; + switch (post_type) { + case '/comments': + kind = 't1'; + break; + case '/submitted': + kind = 't3'; + break; + default: + post_type = ''; + kind = ''; + } + + let sortby = req.query.sort; + let past = req.query.t; + + if (!sortby) { + sortby = 'new'; + } + + if (!['hot', 'new', 'controversial', 'top'].includes(sortby)) { + console.log(`Got invalid sort.`, req.originalUrl); + return res.redirect(`/u/${user}`); + } + + if (past) { + if (sortby === 'controversial' || sortby === 'top') { + if (!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { + console.error(`Got invalid past.`, req.originalUrl); + return res.redirect(`/u/${user}/${sortby}`); + } + } else { + past = ''; + } + } else { + if (sortby === 'controversial' || sortby === 'top') { + past = 'all'; + } else { + past = ''; + } + } + + let key = `${user}:${after}:${before}:sort:${sortby}:past:${past}:post_type:${post_type}:raw_json:${raw_json}`; + redis.get(key, (error, json) => { + if (error) { + console.error(`Error getting the user ${key} key from redis.`, error); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got user ${user} key from redis.`); + (async () => { + if (api_req) { + return handleTedditApiUser( + json, + req, + res, + 'redis', + api_type, + api_target, + user, + after, + before + ); + } else { + let processed_json = await processJsonUser( + json, + false, + after, + before, + req.cookies, + kind, + post_type + ); + return res.render('user', { + data: processed_json, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_config: config, + }); + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/user/${user}/about?raw_json=${raw_json}`; + else + url = `https://reddit.com/user/${user}/about.json?raw_json=${raw_json}`; + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + user_data.about = json; + let url = ''; + if (config.use_reddit_oauth) { + let endpoint = '/overview'; + if (post_type !== '') endpoint = post_type; + url = `https://oauth.reddit.com/user/${user}${post_type}?limit=26${d}&sort=${sortby}&t=${past}&raw_json=${raw_json}`; + } else { + url = `https://reddit.com/user/${user}${post_type}.json?limit=26${d}&sort=${sortby}&t=${past}&raw_json=${raw_json}`; + } + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + user_data.overview = json; + redis.setex( + key, + config.setexs.user, + JSON.stringify(user_data), + (error) => { + if (error) { + console.error( + `Error setting the user ${key} key to redis.`, + error + ); + return res.render('frontpage', { + post: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + (async () => { + if (api_req) { + return handleTedditApiUser( + user_data, + req, + res, + 'online', + api_type, + api_target, + user, + after, + before + ); + } else { + let processed_json = await processJsonUser( + user_data, + true, + after, + before, + req.cookies, + kind, + post_type + ); + return res.render('user', { + data: processed_json, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_config: config, + }); + } + })(); + } + } + ); + }).catch(error => { + console.error(`Something went wrong while fetching data from Reddit API: invalid or non-JSON data was returned.`); + return res.render('frontpage', { + json: null, + http_status_code: 500, + http_statustext: "Invalid response from Reddit", + user_preferences: req.cookies, + instance_config: config, + }); + }); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the overview JSON file from reddit.com/u/${user}`, + error + ); + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + }); + }); + } else { + if (result.status === 404) { + console.log('404 – User not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + http_statustext: result.statusText, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the about JSON file from reddit.com/u/${user}`, + error + ); + }); + } + }); +}); + +userRoutes.get('/user/:user/m/:custom_feed', (req, res, next) => { + res.redirect(`/u/${req.params.user}/m/${req.params.custom_feed}`); +}); + +userRoutes.get('/u/:user/m/:custom_feed/:sort?', (req, res, next) => { + let user = req.params.user; + let custom_feed = req.params.custom_feed; + let subreddit = `u/${user}/m/${custom_feed}`; + let sortby = req.params.sort; + let past = req.query.t; + let before = req.query.before; + let after = req.query.after; + let api_req = req.query.api; + let api_type = req.query.type; + let api_target = req.query.target; + + if (req.query.hasOwnProperty('api')) api_req = true; + else api_req = false; + + let d = `&after=${after}`; + if (before) { + d = `&before=${before}`; + } + + if (!sortby) { + sortby = 'hot'; + } + + if ( + !['new', 'rising', 'controversial', 'top', 'gilded', 'hot'].includes(sortby) + ) { + console.log(`Got invalid sort.`, req.originalUrl); + return res.redirect(`/u/${user}`); + } + + if (past) { + if (sortby === 'controversial' || sortby === 'top') { + if (!['hour', 'day', 'week', 'month', 'year', 'all'].includes(past)) { + console.error(`Got invalid past.`, req.originalUrl); + return res.redirect(`/u/${user}/${sortby}`); + } + } else { + past = undefined; + } + } else { + if (sortby === 'controversial' || sortby === 'top') { + past = 'day'; + } + } + + let key = `${user.toLowerCase()}:m:${custom_feed}:${after}:${before}:sort:${sortby}:past:${past}`; + redis.get(key, (error, json) => { + if (error) { + console.error( + `Error getting the ${user} custom_feed key from redis.`, + error + ); + return res.render('frontpage', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } + if (json) { + console.log(`Got /u/${user} custom_feed key from redis.`); + (async () => { + if (api_req) { + return handleTedditApiSubreddit( + json, + req, + res, + 'redis', + api_type, + api_target, + subreddit, + 'full' + ); + } else { + let processed_json = await processJsonSubreddit( + json, + 'redis', + null, + req.cookies + ); + if (!processed_json.error) { + return res.render('subreddit', { + json: processed_json, + subreddit: '../' + subreddit, + subreddit_about: null, + subreddit_front: !before && !after ? true : false, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + redis_key: key, + after: req.query.after, + before: req.query.before, + instance_config: config, + }); + } else { + return res.render('subreddit', { + json: null, + error: true, + data: processed_json, + user_preferences: req.cookies, + instance_config: config, + }); + } + } + })(); + } else { + let url = ''; + if (config.use_reddit_oauth) + url = `https://oauth.reddit.com/${subreddit}/${sortby}?api_type=json&count=25&g=GLOBAL&t=${past}${d}`; + else + url = `https://reddit.com/${subreddit}/${sortby}.json?api_type=json&count=25&g=GLOBAL&t=${past}${d}`; + fetch(encodeURI(url), redditApiGETHeaders()) + .then((result) => { + if (result.status === 200) { + result.json().then((json) => { + redis.setex( + key, + config.setexs.subreddit, + JSON.stringify(json), + (error) => { + if (error) { + console.error( + `Error setting the ${subreddit} key to redis.`, + error + ); + return res.render('subreddit', { + json: null, + user_preferences: req.cookies, + instance_config: config, + }); + } else { + console.log( + `Fetched the JSON from reddit.com/r/${subreddit}.` + ); + (async () => { + if (api_req) { + return handleTedditApiSubreddit( + json, + req, + res, + 'from_online', + api_type, + api_target, + subreddit, + 'full' + ); + } else { + let processed_json = await processJsonSubreddit( + json, + 'from_online', + null, + req.cookies + ); + return res.render('subreddit', { + json: processed_json, + subreddit: '../' + subreddit, + subreddit_about: null, + subreddit_front: !before && !after ? true : false, + sortby: sortby, + past: past, + user_preferences: req.cookies, + instance_nsfw_enabled: config.nsfw_enabled, + redis_key: key, + after: req.query.after, + before: req.query.before, + instance_config: config, + }); + } + })(); + } + } + ); + }); + } else { + if (result.status === 404) { + console.log('404 – Subreddit not found'); + } else { + console.error( + `Something went wrong while fetching data from Reddit. ${result.status} – ${result.statusText}` + ); + console.error(config.reddit_api_error_text); + } + return res.render('frontpage', { + json: null, + http_status_code: result.status, + user_preferences: req.cookies, + instance_config: config, + }); + } + }) + .catch((error) => { + console.error( + `Error fetching the JSON file from reddit.com/${subreddit}.`, + error + ); + }); + } + }); +}); + +module.exports = userRoutes; diff --git a/static/css/nord.css b/static/css/nord.css new file mode 100644 index 00000000..6d606282 --- /dev/null +++ b/static/css/nord.css @@ -0,0 +1,211 @@ +:root { + --darkbg: #2E3440; + --darkbglight: #434C5E; + --darklinkcolor: #599bff; +} + +body.nord { + background: var(--darkbg); + color: #ECEFF4; +} +body.nord nav { + background: #3E4450; +} +body.nord .top-links a { + background: var(--darkbg); + color: #bfbfbf; +} +body.nord header { + background: var(--darkbglight); + color: #f1f1f1; +} +body.nord #post header div a { + color: var(--darklinkcolor); + text-decoration: none; +} +body.nord a { + color: #f5f5f5; +} +body.nord a:hover, body.nord a:focus { + color: #88C0D0; + text-decoration: underline; +} +body.nord #post header div a:hover, +body.nord #post header div a:focus { + text-decoration: underline; +} +body.nord input[type="submit"]:hover, +body.nord input[type="submit"]:focus, +body.nord .btn:hover, +body.nord .btn:focus { + background: #ECEFF4; + color: #2E3440; + text-decoration: none; +} +body.nord form legend { + border-bottom: 1px solid #353535; +} +body.nord #post .title a { + color: var(--darklinkcolor); +} +body.nord #post .submitted { + color: #a5a5a5; +} +body.nord #post .usertext-body { + background: #2A2935; + border: 1px solid #404040; +} +body.nord .infobar { + background-color: #d2d2d2; + color: #2f2f2f; +} +body.nord .infobar.blue { + background: #c7e3f9; + border: 1px solid #4b78a4; +} +body.nord .infobar a { + color: #0356d4; +} +body.nord header .tabmenu li a { + background: #3e3e3e; +} +body.nord header .tabmenu li a:hover, body.nord header .tabmenu li a:focus { + text-decoration: underline; + color: #ECEFF4; +} +body.nord #search { + color: #d2d2d2; +} +body.nord .md { + color: #dadada; +} +body.nord .md blockquote, body.nord .md del { + color: #777777; +} +body.nord .md code, body.nord .md pre { + background: #2E3440; + color: #cacaca; +} +body.nord .comment .body blockquote { + background: #2E3440; + color: #afafaf; + border-color: #464646; +} +body.nord .even-depth { + background: var(--darkbg); +} +body.nord .odd-depth { + background: var(--darkbglight); +} + +body.nord .comment .comment { + border-left: 1px solid #545454; +} + +body.nord .comment .meta .created a { + color: #7b7b7b; +} +body.nord .comment details summary { + color: #868686; +} +body.nord .comment details summary::-webkit-details-marker, +body.nord .comment details summary::marker { + color: #868686; +} +body.nord #links .link .entry .title a h2 { + color: #f0f0f0; +} +body.nord #links .link .entry .title a:visited h2 { + color: #8FBCBB; +} +body.nord #links .link .image .no-image, +body.nord #user .entry .image .no-image { + filter: opacity(0.5); +} +body.nord #user .comment { + width: 100%; + background: var(--darkbg); +} +body.nord #links .link .upvotes { + color: #D08770; +} +body.nord .upvotes .arrow, +body.nord .score .arrow { + filter: opacity(0.5); +} +body.nord #links .link .entry .meta a { + color: #81A1C1; +} +#links .link .entry .meta { + color: #D8DEE9 !important; +} +#links .link .entry .title span { + color: #5E81AC !important; +} +body.nord #links .link .entry .selftext { + background: #2A2935; + border: 1px solid #404040; +} +body.nord #links .link .entry .meta .links .selftext a { + color: var(--darklinkcolor); + margin: 0; +} +body.nord #links .link .entry details .line { + width: 16px; + margin-top: 3px; + background: #4C566A; + border: 1px solid #81A1C1; +} +body.nord .content .bottom img { + filter: invert(1); +} +body.nord .container .content { + border: 1px solid #434C5E; +} +body.nord input[type="submit"], +body.nord .btn { + background: #2E3440; + color: #ECEFF4; +} +body.nord #post .crosspost { + background: var(--darkbg); +} +body.nord .view-more-links a { + background: #2E3440; + color: #cacaca; +} +body.nord .md .md-spoiler-text:not(.revealed):active, +body.nord .md .md-spoiler-text:not(.revealed):focus, +body.nord .md .md-spoiler-text:not(.revealed):hover { + background: #cacaca; + color: #2E3440; +} +body.nord .comment .body a, +body.nord .usertext-body a { + color: #3d99fb; +} +body.nord header .tabmenu li.active a { + background: #acacac; + color: #151515; +} +body.nord #search form input[type="text"] { + background: #2E3440; + color: #ECEFF4; +} +body.nord footer { + background: #2f2f2f; +} +body.nord footer a { + color: #999; +} +body.nord .flair { + color: #eaeaea !important; + background-color: #404040 !important; +} +body.nord #sr-more-link { + color: #ECEFF4; + background: #2E3440; +} +body.nord #post .usertext-body .poll { + border: 1px solid #404040; +} diff --git a/static/css/styles.css b/static/css/styles.css index 7a732aab..00745bdd 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -84,6 +84,7 @@ nav .nav-item.left a { } nav .nav-item.left img { width: 20px; + height: 20px; vertical-align: bottom; margin: 0 7px 0 0; } @@ -280,9 +281,11 @@ header .tabmenu li.active a { color: white; padding: 16px; } +input, select { + padding: 2px; +} input[type="submit"], .btn { - padding: 3px; margin-top: 7px; margin-right: 10px; border-radius: 0; @@ -378,6 +381,18 @@ footer a { max-height: 80px; float: left; text-align: center; + position: relative; +} +#links .link .image .duration { + left: 0; + background: gray; + position: absolute; + background-color: rgba(0,0,0,0.6); + color: white; + bottom: 0px; + font-size: var(--sm-font); + width: 100%; + text-align: center; } #links .link .image img { width: auto; @@ -566,6 +581,9 @@ footer a { overflow: hidden; background: var(--whitebg); } +.commententry .comment { + padding-left: 0; +} .comment details summary { float: left; font-size: 0.833rem; @@ -734,6 +752,14 @@ footer a { #post .submitted span { margin-left: 5px; } +#post .source-details { + float: left; + margin: 10px 0 10px 30px; +} +#post .source-details summary:hover { + color: var(--linkcolor); + text-decoration: underline; +} #post .comments { float: left; width: 100%; @@ -924,14 +950,14 @@ footer a { width: 80%; min-height: 100vh; } -#user .entries .entry { +#user .entries .commententry { padding-left: 5px; padding-top: 10px; padding-bottom: 15px; float: left; width: 100%; } -#user .entries .entry:first-child { +#user .entries .commententry:first-child { padding-top: 0; } #user .info { @@ -947,70 +973,70 @@ footer a { font-size: 1.1rem; overflow-wrap: anywhere; } -#user .entries .entry .meta { +#user .entries .commententry .meta { float: left; } -#user .entries .entry .meta .title, -#user .entries .entry .meta .author, -#user .entries .entry .meta .subreddit, -#user .entries .entry .meta .flair { +#user .entries .commententry .meta .title, +#user .entries .commententry .meta .author, +#user .entries .commententry .meta .subreddit, +#user .entries .commententry .meta .flair { float: left; } -#user .entries .entry .meta a { +#user .entries .commententry .meta a { margin-right: 5px; margin-left: 5px; } -#user .entries .entry .title a { +#user .entries .commententry .title a { margin-left: 0; font-size: 0.86rem; } -#user .entries .entry .meta .author,#user .entries .entry .meta .subreddit { +#user .entries .commententry .meta .author,#user .entries .commententry .meta .subreddit { font-size: 11px; margin-top: 3px; } -#user .entries .entry .meta .author a { +#user .entries .commententry .meta .author a { font-weight: bold; } -#user .comment details { +#user .commententry details { padding-top: 2px; } -#user .comment details a.context, -#user .comment details a.comments { +#user .commententry details a.context, +#user .commententry details a.comments { float: left; } -#user .comment .meta p.ups,#user .comment .meta p.created { +#user .commententry .meta p.ups,#user .commententry .meta p.created { font-size: var(--sm-font); padding-right: 5px; } -#user .entries .entry .meta .created a { +#user .entries .commententry .meta .created a { color: var(--graytext); } -#user .entries .entry.t3 .title .meta { +#user .entries .commententry.t3 .title .meta { float: left; width: 100%; } -#user .entries .entry.t3 .title a { +#user .entries .commententry.t3 .title a { margin-bottom: 3px; } -#user .entries .entry.t3 .upvotes { +#user .entries .commententry.t3 .upvotes { float: left; width: 60px; } -#user .entries .entry.t3 .image { +#user .entries .commententry.t3 .image { float: left; width: 80px; } -#user .entries .entry.t3 .title { +#user .entries .commententry.t3 .title { width: calc(100% - 200px); float: left; } -#user .entries .entry .comment .meta .author { +#user .entries .commententry .commententry .meta .author { margin-top: 0; } -#user .comment .meta p { +#user .commententry .meta p { padding-right: 0; } -#user .comment .body { +#user .commententry .body { padding-top: 4px; padding-bottom: 0; } @@ -1018,26 +1044,25 @@ footer a { font-weight: bold; font-size: 1.1rem; } -#user .comment details summary { +#user .commententry details summary { font-size: var(--sm-font); } -#user .comment details summary p { +#user .commententry details summary p { margin-right: 5px; margin-left: 5px; } -#user .comment details summary a { +#user .commententry details summary a { margin-left: 5px; } -#user .entries .entry .image,#user .entries .entry .upvotes,#user .entries .entry .title,#user .entries .entry .meta { +#user .entries .commententry .image,#user .entries .commententry .upvotes,#user .entries .commententry .title,#user .entries .commententry .meta { float: left; } -#user .entries .entry .image { +#user .entries .commententry .image { margin-left: 0; margin-right: 8px; position: relative; } -#user .entries .entry .image a span { - position: absolute; +#user .entries .link .image a span { bottom: 0; background: #0000005e; left: 0; @@ -1047,29 +1072,38 @@ footer a { font-size: var(--sm-font); margin-bottom: 4px; } -#user .entries .entry .image img { +#user .entries .link .image a span { + bottom: 0; + background: #0000005e; + left: 0; + width: 100%; + text-align: center; + color: white; + font-size: var(--sm-font); +} +#user .entries .link .image img { max-width: 80px; } -#user .entries .entry .title a { +#user .entries .commententry .title a { float: left; } -#user .entries .entry .title .meta { +#user .entries .commententry .title .meta { width: 100%; } -#user .entries .entry .title .meta a { +#user .entries .commententry .title .meta a { float: initial; font-weight: bold; font-size: var(--sm-font); margin-left: 5px; } -#user .entries .entry .title .meta a.subreddit { +#user .entries .commententry .title .meta a.subreddit { font-weight: unset; } -#user .entries .entry .title .meta .submitted { +#user .entries .commententry .title .meta .submitted { font-size: var(--sm-font); color: var(--graytext); } -#user .entries .entry .meta .title { +#user .entries .commententry .meta .title { margin-left: 20px; } #user #links { @@ -1085,18 +1119,18 @@ footer a { #user #links details ul { margin-left: 20px; } -#user .entries .entry a.comments, #user .entries .entry a.context { +#user .entries .commententry a.comments, #user .entries .commententry a.context { color: gray; font-size: var(--sm-font); font-weight: bold; } -#user .entries .entry .title .meta a.comments { +#user .entries .commententry .title .meta a.comments { margin-left: 0; } -#user .entries .entry a.comments.t1,#user .entries .entry a.context { +#user .entries .commententry a.comments.t1,#user .entries .commententry a.context { margin-top: 0; } -#user .entries .entry a.context { +#user .entries .commententry a.context { margin-right: 10px; } /* FLAIR */ @@ -1549,6 +1583,103 @@ code { padding:4px; margin:5px 0 } + +/* "CLEANED HOMEPAGE" SECTION */ +body.homepage.clean { + margin: 0; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +body.homepage.clean main { + flex-grow: 1; + display: flex; + width: 100%; + flex-direction: column; + justify-content: center; + align-items: center; +} + +body.homepage.clean h1 { + margin-bottom: 1rem; + font-size: 3rem; + text-align: center; + width: 100%; +} + +body.homepage.clean form { + width: 100vw; + max-width: 750px; + text-align: center; +} + +body.homepage.clean input[name="q"] { + width: 90%; + padding: 0.4rem; + border: none; + color: white; + background: #555; + margin-bottom: 1rem; +} + +body.homepage.clean .sublinks { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + max-width: 650px; +} + +body.homepage.clean .sublinks a { + color: gray; + margin-right: 0.3rem; +} + +.homepage.clean .top-links { + display: none; +} + +@media only screen and (max-width: 768px) { + body.homepage.clean form, body.homepage.clean .sublinks { + width: 90%; + max-width: unset; + } +} + +/* Large gallery items */ +.gallery .item.large { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + position: relative; + margin-right: 0.3rem; +} + +.gallery .item.large img { + max-height: 90vh; + position: relative; +} + +.gallery .item.large .caption { + position: absolute; + width: calc(100% - 0.6rem); + color: white; + background: rgba(0, 0, 0, 0.7); + padding: 0.3rem; + bottom: 0; +} + +@media only screen and (max-width: 768px) { + .gallery .item.large img { + max-height: unset; + max-width: 100%; + } +} + /* Fix spoiler texts not showing without JS */ .md .md-spoiler-text:not(.revealed):active,.md .md-spoiler-text:not(.revealed):focus,.md .md-spoiler-text:not(.revealed):hover { color: black; diff --git a/static/instances.json b/static/instances.json new file mode 120000 index 00000000..7d79ae80 --- /dev/null +++ b/static/instances.json @@ -0,0 +1 @@ +../instances.json \ No newline at end of file diff --git a/static/logo512.png b/static/logo512.png new file mode 100644 index 00000000..fcb5afee Binary files /dev/null and b/static/logo512.png differ diff --git a/static/robots.txt b/static/robots.txt index 23132a0b..18a23440 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -1,2 +1,3 @@ User-agent: MJ12bot +User-agent: Applebot Disallow: / diff --git a/views/about.pug b/views/about.pug index eba33b14..954258be 100644 --- a/views/about.pug +++ b/views/about.pug @@ -2,6 +2,9 @@ doctype html html head title about - teddit + meta(property='og:title', content='about - teddit') + include includes/meta_default.pug + include includes/meta_description.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug @@ -18,8 +21,10 @@ html a(href="/privacy") Privacy policy h2 Donating p(class="word-break") XMR: 832ogRwuoSs2JGYg7wJTqshidK7dErgNdfpenQ9dzMghNXQTJRby1xGbqC3gW3GAifRM9E84J91VdMZRjoSJ32nkAZnaCEj + h2 Legal + p Teddit does not host any content. All content shown on any Teddit instances is from Reddit™. Reddit is a trademark of Reddit Inc. Teddit is not affiliated with Reddit Inc. Any issues with content shown on any Teddit instances need to be reported to Reddit, not the instance host's internet provider or domain provider. .bottom a(href="https://en.wikipedia.org/wiki/Piratbyr%C3%A5n#Kopimi", target="_blank") img(src="kopimi.gif") - p.version v.0.3.1 + p.version v.0.4.9 include includes/footer.pug diff --git a/views/components/link.pug b/views/components/link.pug new file mode 100644 index 00000000..42e7e3e5 --- /dev/null +++ b/views/components/link.pug @@ -0,0 +1,100 @@ +.link + if user_preferences.show_upvotes === 'false' + style. + .upvotes { + display: none; + } + .upvotes + .arrow + span #{kFormatter(link.ups)} + .arrow.down + .image + if link.images + if link.is_self_link + a(href="" + link.permalink + "") + img(src="" + link.images.thumb + "", alt="") + if link.duration + span.duration #{secondsToMMSS(link.duration)} + else + a(href=""+ link.url +"", rel="noopener noreferrer") + img(src="" + link.images.thumb + "", alt="") + if link.duration + span.duration #{secondsToMMSS(link.duration)} + else + a(href="" + link.permalink + "") + .no-image no image + .entry + .title + if link.is_self_link + a(href="" + link.permalink + "") + h2(class="" + (link.stickied ? 'green' : '') + "") #{cleanTitle(link.title)} + != link.link_flair + span (#{link.domain}) + else + a(href="" + link.url + "", rel="noopener noreferrer") + h2(class="" + (link.stickied ? 'green' : '') + "") #{cleanTitle(link.title)} + != link.link_flair + span (#{link.domain}) + .meta + p.submitted submitted + span(title="" + toUTCString(link.created) + "") #{timeDifference(link.created)} by + if link.author === '[deleted]' + span(class="deleted") [deleted] + else + a(href="/u/" + link.author + "") + | #{link.author} + != link.user_flair + p.to to + a(href="/r/" + link.subreddit + "") + | #{link.subreddit} + if link.stickied + span(class="green") stickied + .links + if link.over_18 + span.tag.nsfw NSFW + if link.selftext_html + details + summary + .line + .line + .line + .selftext + != unescape(link.selftext_html, user_preferences) + if (link.images && link.images.preview) + style. + details.preview-container img { + width: 100% !important; + height: auto !important; + max-width: none !important; + max-height: none !important; + opacity: 0; + } + details.preview-container[open][data-url="#{link.images.preview}"] .preview { + width: 100%; + height: auto; + background-image: url('#{link.images.preview}'); + background-repeat: no-repeat; + background-size: contain; + } + details.preview-container(data-url="" + link.images.preview + "") + summary + span ▶ + .preview + img(src=""+ link.images.thumb +"", alt="") + a(href="" + link.permalink + "", class="comments") #{link.num_comments} comments + - + let back_url = "/r/" + subreddit + "/" + sortby + "§2t="+ (past ? past : '') +"" + if(before && !subreddit_front) + back_url = "/r/" + subreddit + "/" + sortby + "§2t="+ (past ? past : '') +"§1before=" + before + "" + if(after) + back_url = "/r/" + subreddit + "/" + sortby + "§2t=" + (past ? past : '') + "§1after=" + after + "" + - let saved_post = false + if user_preferences.saved + each post_id in user_preferences.saved + if post_id === link.id + - saved_post = true + if saved_post + a(href="/unsave/" + link.id + "/?rk=" + redis_key + "&b=" + back_url + "") unsave + else + a(href="/save/" + link.id + "/?rk=" + redis_key + "&b=" + back_url + "") save + a(href=("https://www.reddit.com" + link.permalink) target="_blank" title="Open in Reddit (new tab)") [R↗] diff --git a/views/frontpage.pug b/views/frontpage.pug new file mode 100644 index 00000000..ad973b79 --- /dev/null +++ b/views/frontpage.pug @@ -0,0 +1,92 @@ +doctype html +html + head + title teddit + meta(property='og:title', content='frontpage : teddit') + include includes/meta_default.pug + include includes/meta_description.pug + include includes/head.pug + body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") + include includes/topbar.pug + if json === null + .reddit-error + h2 Error + p #{JSON.stringify(http_status_code)} + p #{JSON.stringify(http_statustext)} + if http_status_code == "401" || http_status_code == "503" + p This error is probably caused because Reddit itself is down or having server issues. + p Checking https://www.redditstatus.com might give some information. + if http_status_code == "404" + p The resource you were looking for was not found. + else + - var subreddit = '' + if(user_preferences.subbed_subreddits && Array.isArray(user_preferences.subbed_subreddits)) + - subreddit = '/r/' + user_preferences.subbed_subreddits.join('+') + header + a(href="/", class="main") + h1 teddit + .bottom + ul.tabmenu + li(class=!sortby || sortby == 'hot' ? 'active' : '') + a(href="" + subreddit + "/") hot + li(class=sortby === 'new' ? 'active' : '') + a(href="" + subreddit + "/new") new + li(class=sortby === 'rising' ? 'active' : '') + a(href="" + subreddit + "/rising") rising + li(class=sortby === 'controversial' ? 'active' : '') + a(href="" + subreddit + "/controversial") controversial + li(class=sortby === 'top' ? 'active' : '') + a(href="" + subreddit + "/top") top + if !before && !after && sortby === 'hot' + #intro + h1 Welcome to teddit + h2 the alternative, privacy respecting, front page of internet. + + #links.sr + if sortby === 'top' || sortby === 'controversial' + details + summary + if past === 'hour' + span links from: past hour + if past === 'day' + span links from: past 24 hours + if past === 'week' + span links from: past week + if past === 'month' + span links from: past month + if past === 'year' + span links from: past year + if past === 'all' + span links from: all time + ul + li(class=past === 'hour' ? 'active' : '') + a(href="?t=hour") past hour + li(class=past === 'day' ? 'active' : '') + a(href="?t=day") past 24 hours + li(class=past === 'week' ? 'active' : '') + a(href="?t=week") past week + li(class=past === 'month' ? 'active' : '') + a(href="?t=month") past month + li(class=past === 'year' ? 'active' : '') + a(href="?t=year") past year + li(class=past === 'all' ? 'active' : '') + a(href="?t=all") all time + each link in json.links + include components/link.pug + if json.info.before || json.info.after + .view-more-links + - var subreddit = 'all' + if(user_preferences.subbed_subreddits && Array.isArray(user_preferences.subbed_subreddits)) + - subreddit = user_preferences.subbed_subreddits.join('+') + if json.info.after + a(href="/r/" + subreddit + "/" + sortby + "?t=" + (past ? past : '') + "&after=" + json.info.after + "") next › + #search + form(action="/r/all/search", method="GET") + div + label(for="q") search + input(type="text", name="q", id="q", placeholder="search") + div + label(for="nsfw") include NSFW results + input(type="checkbox", name="nsfw", id="nsfw", checked="checked") + input(type="submit", value="search") + include includes/footer.pug diff --git a/views/homepage.pug b/views/homepage.pug new file mode 100644 index 00000000..2e97b87e --- /dev/null +++ b/views/homepage.pug @@ -0,0 +1,33 @@ +doctype html +html + head + title teddit + meta(property='og:title', content='home : teddit') + include includes/meta_default.pug + include includes/meta_description.pug + include includes/head.pug + body(class="" + (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + " homepage clean") + include includes/topbar.pug + main + h1 teddit + form(action="/search", method="GET") + input(type="text", name="q") + input(type="hidden", name="restrict_sr", value="on") + input(type="hidden", name="nsfw", value="on") + input(type="hidden", name="sort", value="relevance") + input(type="hidden", name="t", value="all") + .sublinks + if user_preferences.subbed_subreddits && Array.isArray(user_preferences.subbed_subreddits) + a(href="/r/popular") Popular + a(href="/r/all") All + a(href="/saved") Saved + each subreddit in user_preferences.subbed_subreddits + a(href="/r/" + subreddit) #{subreddit} + else if instance_config.suggested_subreddits && Array.isArray(instance_config.suggested_subreddits) + each subreddit in instance_config.suggested_subreddits + if subreddit.toLowerCase() === 'saved' + a(href="/saved") Saved + else + a(href="/r/" + subreddit) #{subreddit} + include includes/footer.pug + diff --git a/views/includes/head.pug b/views/includes/head.pug index 2bf6ee8e..44bad4e9 100644 --- a/views/includes/head.pug +++ b/views/includes/head.pug @@ -4,8 +4,11 @@ if(user_preferences.theme === 'dark') link(rel="stylesheet", type="text/css", href="/css/dark.css") if(user_preferences.theme === 'sepia') link(rel="stylesheet", type="text/css", href="/css/sepia.css") +if(user_preferences.theme === 'nord') + link(rel="stylesheet", type="text/css", href="/css/nord.css") link(rel="stylesheet", type="text/css", href="/css/styles.css") link(rel="icon", type="image/png", sizes="32x32", href="/favicon.png") +meta(property='og:site_name', content='teddit') meta(name="viewport", content="width=device-width, initial-scale=1.0") - if(!user_preferences) diff --git a/views/includes/meta_default.pug b/views/includes/meta_default.pug new file mode 100644 index 00000000..1f8e8c28 --- /dev/null +++ b/views/includes/meta_default.pug @@ -0,0 +1,3 @@ +meta(property='og:type', content='website') +meta(name='twitter:card', content='summary') +include meta_logo.pug diff --git a/views/includes/meta_description.pug b/views/includes/meta_description.pug new file mode 100644 index 00000000..921dafe8 --- /dev/null +++ b/views/includes/meta_description.pug @@ -0,0 +1 @@ +meta(property='og:description', content='Teddit is a free and open source alternative Reddit front-end focused on privacy. Teddit doesn\'t require you to have JavaScript enabled in your browser.') diff --git a/views/includes/meta_logo.pug b/views/includes/meta_logo.pug new file mode 100644 index 00000000..876358d2 --- /dev/null +++ b/views/includes/meta_logo.pug @@ -0,0 +1,4 @@ +meta(property='og:image', content='/logo512.png') +meta(property='og:image:width', content='128') +meta(property='og:image:height', content='128') +meta(property='og:author_name', content='') diff --git a/views/includes/meta_post.pug b/views/includes/meta_post.pug new file mode 100644 index 00000000..1577ab56 --- /dev/null +++ b/views/includes/meta_post.pug @@ -0,0 +1,43 @@ +meta(property='og:title', content=cleanTitle(post.title) + ' : r/' + subreddit) +meta(property='og:description', content='' + post.selftext_preview) +meta(property='og:author_name', content='u/' + post.author) +if !post.has_media + if post.gallery + meta(property='twitter:card', content='summary_large_image') + meta(property='og:type', content='image') + each item in post.gallery_items + meta(property='og:image', content='' + item.large) + else + if post.images + meta(property='twitter:card', content='summary_large_image') + meta(property='og:type', content='image') + meta(property='og:image', content='' + post.images.source) + meta(property='og:image:url', content='' + post.images.source) + else + include meta_logo.pug +else + if post.media + if post.media.not_hosted_in_reddit + if post.media.source === 'YouTube' + meta(property='twitter:card', content='player') + meta(property='og:type', content='video') + meta(property='og:video', content='' + post.media.embed_src) + else + if post.media.source === 'external' + if post.images + meta(name='twitter:card', content='summary_large_image') + meta(property='og:type', content='image') + meta(property='og:image', content='' + post.images.source) + meta(property='og:image:src', content='' + post.images.source) + else + meta(name='twitter:card', content='summary_large_image') + meta(property='og:type', content='image') + meta(property='og:image', content='' + post.media.source) + meta(property='og:image:url', content='' + post.media.source) + else + meta(property='twitter:card', content='player') + meta(property='og:type', content='video') + meta(property='og:video', content='' + post.media.source) + meta(property='og:video:type', content='video/mp4') + else + include meta_logo.pug diff --git a/views/includes/topbar.pug b/views/includes/topbar.pug index b5078324..f060e3ed 100644 --- a/views/includes/topbar.pug +++ b/views/includes/topbar.pug @@ -16,55 +16,10 @@ div#topbar a(href="/saved") Saved each subreddit in user_preferences.subbed_subreddits a(href="/r/" + subreddit) #{subreddit} - else - a(href="/r/popular") Popular - a(href="/r/all") All - a(href="/saved") Saved - a(href="/r/AskReddit") AskReddit - a(href="/r/pics") pics - a(href="/r/news") news - a(href="/r/worldnews") worldnews - a(href="/r/funny") funny - a(href="/r/tifu") tifu - a(href="/r/videos") videos - a(href="/r/gaming") gaming - a(href="/r/aww") aww - a(href="/r/todayilearned") todayilearned - a(href="/r/gifs") gifs - a(href="/r/Art") Art - a(href="/r/explainlikeimfive") explainlikeimfive - a(href="/r/movies") movies - a(href="/r/Jokes") Jokes - a(href="/r/TwoXChromosomes") TwoXChromosomes - a(href="/r/mildlyinteresting") mildlyinteresting - a(href="/r/LifeProTips") LifeProTips - a(href="/r/askscience") askscience - a(href="/r/IAmA") IAmA - a(href="/r/dataisbeautiful") dataisbeautiful - a(href="/r/books") books - a(href="/r/science") science - a(href="/r/Showerthoughts") Showerthoughts - a(href="/r/gadgets") gadgets - a(href="/r/Futurology") Futurology - a(href="/r/nottheonion") nottheonion - a(href="/r/history") history - a(href="/r/sports") sports - a(href="/r/OldSchoolCool") OldSchoolCool - a(href="/r/GetMotivated") GetMotivated - a(href="/r/DIY") DIY - a(href="/r/photoshopbattles") photoshopbattles - a(href="/r/nosleep") nosleep - a(href="/r/Music") Music - a(href="/r/space") space - a(href="/r/food") food - a(href="/r/UpliftingNews") UpliftingNews - a(href="/r/EarthPorn") EarthPorn - a(href="/r/Documentaries") Documentaries - a(href="/r/InternetIsBeautiful") InternetIsBeautiful - a(href="/r/WritingPrompts") WritingPrompts - a(href="/r/creepy") creepy - a(href="/r/philosophy") philosophy - a(href="/r/announcements") announcements - a(href="/r/listentothis") listentothis - a(href="/r/blog") blog + else if instance_config.suggested_subreddits && Array.isArray(instance_config.suggested_subreddits) + each subreddit in instance_config.suggested_subreddits + if subreddit.toLowerCase() === 'saved' + a(href="/saved") Saved + else + a(href="/r/" + subreddit) #{subreddit} a(href="/subreddits", id="sr-more-link") more » diff --git a/views/index.pug b/views/index.pug deleted file mode 100644 index 32b30e3e..00000000 --- a/views/index.pug +++ /dev/null @@ -1,172 +0,0 @@ -doctype html -html - head - title teddit - include includes/head.pug - body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") - include includes/topbar.pug - if json === null - .reddit-error - h2 Error - p #{JSON.stringify(http_status_code)} - p #{JSON.stringify(http_statustext)} - if http_status_code == "401" || http_status_code == "503" - p This error is probably caused because Reddit itself is down or having server issues. - p Checking https://www.redditstatus.com might give some information. - if http_status_code == "404" - p The resource you were looking for was not found. - else - - var subreddit = '' - if(user_preferences.subbed_subreddits && Array.isArray(user_preferences.subbed_subreddits)) - - subreddit = '/r/' + user_preferences.subbed_subreddits.join('+') - header - a(href="/", class="main") - h1 teddit - .bottom - ul.tabmenu - li(class=!sortby || sortby == 'hot' ? 'active' : '') - a(href="" + subreddit + "/") hot - li(class=sortby === 'new' ? 'active' : '') - a(href="" + subreddit + "/new") new - li(class=sortby === 'rising' ? 'active' : '') - a(href="" + subreddit + "/rising") rising - li(class=sortby === 'controversial' ? 'active' : '') - a(href="" + subreddit + "/controversial") controversial - li(class=sortby === 'top' ? 'active' : '') - a(href="" + subreddit + "/top") top - if !before && !after && sortby === 'hot' - #intro - h1 Welcome to teddit - h2 the alternative, privacy respecting, front page of internet. - #links.sr - if sortby === 'top' || sortby === 'controversial' - details - summary - if past === 'hour' - span links from: past hour - if past === 'day' - span links from: past 24 hours - if past === 'week' - span links from: past week - if past === 'month' - span links from: past month - if past === 'year' - span links from: past year - if past === 'all' - span links from: all time - ul - li(class=past === 'hour' ? 'active' : '') - a(href="?t=hour") past hour - li(class=past === 'day' ? 'active' : '') - a(href="?t=day") past 24 hours - li(class=past === 'week' ? 'active' : '') - a(href="?t=week") past week - li(class=past === 'month' ? 'active' : '') - a(href="?t=month") past month - li(class=past === 'year' ? 'active' : '') - a(href="?t=year") past year - li(class=past === 'all' ? 'active' : '') - a(href="?t=all") all time - each link in json.links - .link - .upvotes - .arrow - span #{kFormatter(link.ups)} - .arrow.down - .image - if(link.images) - if link.is_self_link - a(href="" + link.permalink + "") - img(src=""+ link.images.thumb +"", alt="") - else - a(href="" + link.url + "", rel="noopener noreferrer") - img(src=""+ link.images.thumb +"", alt="") - else - a(href="" + link.permalink + "") - .no-image no image - .entry - .title - if link.is_self_link - a(href="" + link.permalink + "") - h2 #{cleanTitle(link.title)} - span (#{link.domain}) - else - a(href="" + link.url + "", rel="noopener noreferrer") - h2 #{cleanTitle(link.title)} - span (#{link.domain}) - .meta - p.submitted submitted - span(title="" + toUTCString(link.created) + "") #{timeDifference(link.created)} by - if link.author === '[deleted]' - span(class="deleted") [deleted] - else - a(href="/u/" + link.author + "") - | #{link.author} - span(class="to") to - a(href="/r/" + link.subreddit + "") - | #{link.subreddit} - .links - if link.over_18 - span.tag.nsfw NSFW - if link.selftext_html - details - summary - .line - .line - .line - .selftext - != unescape(link.selftext_html, user_preferences) - if (link.images && link.images.preview) - style. - details.preview-container img { - width: 100% !important; - height: auto !important; - max-width: none !important; - max-height: none !important; - opacity: 0; - } - details.preview-container[open][data-url="#{link.images.preview}"] .preview { - width: 100%; - height: auto; - background-image: url('#{link.images.preview}'); - background-repeat: no-repeat; - background-size: contain; - } - details.preview-container(data-url="" + link.images.preview + "") - summary - span ▶ - .preview - img(src=""+ link.images.thumb +"", alt="") - a(href="" + link.permalink + "", class="comments") #{link.num_comments} comments - - - let back_url = "/" + sortby + "§2t="+ (past ? past : '') +"" - if(before && !subreddit_front) - back_url = "/" + sortby + "§2t="+ (past ? past : '') +"§1before=" + before + "" - if(after) - back_url = "/" + sortby + "§2t=" + (past ? past : '') + "§1after=" + after + "" - - let saved_post = false - if user_preferences.saved - each post_id in user_preferences.saved - if post_id === link.id - - saved_post = true - if saved_post - a(href="/unsave/" + link.id + "/?rk=" + redis_key + "&b=" + back_url + "") unsave - else - a(href="/save/" + link.id + "/?rk=" + redis_key + "&b=" + back_url + "") save - if json.info.before || json.info.after - .view-more-links - - var subreddit = 'all' - if(user_preferences.subbed_subreddits && Array.isArray(user_preferences.subbed_subreddits)) - - subreddit = user_preferences.subbed_subreddits.join('+') - if json.info.after - a(href="/r/" + subreddit + "/" + sortby + "?t=" + (past ? past : '') + "&after=" + json.info.after + "") next › - #search - form(action="/r/all/search", method="GET") - div - label(for="q") search - input(type="text", name="q", id="q", placeholder="search") - div - label(for="nsfw") include NSFW results - input(type="checkbox", name="nsfw", id="nsfw", checked="checked") - input(type="submit", value="search") - include includes/footer.pug diff --git a/views/post.pug b/views/post.pug index 09055d54..07338ef8 100644 --- a/views/post.pug +++ b/views/post.pug @@ -2,6 +2,7 @@ doctype html html head title #{cleanTitle(post.title)} : #{subreddit} + include includes/meta_post.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug @@ -37,7 +38,21 @@ html max-height: #{user_preferences.post_media_max_height}px; max-width: 100%; } + - + let video_muted = false + if(instance_videos_muted === true || user_preferences.videos_muted === 'true') { + video_muted = true + } + if(user_preferences.videos_muted === 'false') { + video_muted = false + } .info + if user_preferences.show_upvotes === 'false' + style. + .comment .meta .ups, #post .score, #user .upvotes { + display: none; + } + .score div.arrow span #{kFormatter(post.ups)} @@ -72,6 +87,7 @@ html a(href="/unsave/" + post.id + "/?rk=" + redis_key + "&b=" + back_url + "") unsave else a(href="/save/" + post.id + "/?rk=" + redis_key + "&b=" + back_url + "") save + a(href=("https://www.reddit.com" + back_url) target="_blank" title="Open in Reddit (new tab)" style="margin-left: 10px") [R↗] if post.crosspost.is_crosspost === true .crosspost .title @@ -113,7 +129,7 @@ html p #{post.media.embed_src} else .video - video(controls="controls", autoplay="autoplay", loop="loop") + video(controls="controls", autoplay="autoplay", loop="loop", muted=(video_muted ? true : false)) source(src="" + post.media.source + "", type="video/mp4") | Your browser does not support the video element. a(href="" + post.media.source + "") [media] @@ -122,12 +138,19 @@ html if post.gallery .gallery each item in post.gallery_items - .item - div + if user_preferences.show_large_gallery_images == 'true' + .item.large a(href="" + item.large + "", target="_blank") - img(src=""+ item.thumbnail +"", alt="") - a(href="" + item.source + "", target="_blank", class="source-link") - small source + img(src="" + item.large + "", title="" + item.caption + "") + if item.caption + span.caption !{item.caption} + else + .item + div + a(href="" + item.large + "", target="_blank") + img(src=""+ item.thumbnail +"", alt="") + a(href="" + item.source + "", target="_blank", class="source-link") + small source if post.images .image a(href="" + post.images.source + "") @@ -155,10 +178,11 @@ html a(href="" + post.media.embed_src + "", target="_blank", rel="noopener noreferrer") img(src="" + post.images.source + "", alt="") if !post.media.embed_src.startsWith("https://twitter.com") - p - | source: - a(href="" + post.media.embed_src + "", target="_blank", rel="noopener noreferrer") - p(class="source-url") #{post.media.embed_src} + details(class="source-details") + summary Source + p + a(href="" + post.media.embed_src + "", target="_blank", rel="noopener noreferrer") + p(class="source-url") #{post.media.embed_src} else .video a(href="" + post.media.source + "") @@ -168,7 +192,7 @@ html p #{post.media.embed_src} else .video - video(controls="controls", autoplay="autoplay", loop="loop") + video(controls="controls", autoplay="autoplay", loop="loop", muted=(video_muted ? true : false)) source(src="" + post.media.source + "", type="video/mp4") | Your browser does not support the video element. a(href="" + post.media.source + "") [media] diff --git a/views/preferences.pug b/views/preferences.pug index 6325086e..b18f2d54 100644 --- a/views/preferences.pug +++ b/views/preferences.pug @@ -2,6 +2,9 @@ doctype html html head title preferences - teddit + meta(property='og:title', content='preferences - teddit') + include includes/meta_default.pug + include includes/meta_description.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug @@ -11,47 +14,69 @@ html form(action="/saveprefs", method="POST") legend Privacy .setting - label(for="domain_twitter") Replace Twitter links with Nitter (blank to disable): + label(for="domain_twitter") Replace Twitter links with Nitter (blank to disable): if(user_preferences.domain_twitter != '' && typeof(user_preferences.domain_twitter) != 'undefined') input(type="text", name="domain_twitter", id="domain_twitter", value="" + user_preferences.domain_twitter + "", placeholder="e.g. nitter.net") else input(type="text", name="domain_twitter", id="domain_twitter", placeholder="e.g. nitter.net") .setting - label(for="domain_youtube") Replace YouTube links with Invidious (blank to disable): + label(for="domain_youtube") Replace YouTube links with Invidious/Piped (blank to disable): if(user_preferences.domain_youtube != '' && typeof(user_preferences.domain_youtube) != 'undefined') input(type="text", name="domain_youtube", id="domain_youtube", value="" + user_preferences.domain_youtube + "", placeholder="e.g. invidious.site") else input(type="text", name="domain_youtube", id="domain_youtube", placeholder="e.g. invidious.site") .setting - label(for="domain_instagram") Replace Instagram links with Bibliogram (blank to disable): - if(user_preferences.domain_instagram != '' && typeof(user_preferences.domain_instagram) != 'undefined') - input(type="text", name="domain_instagram", id="domain_instagram", value="" + user_preferences.domain_instagram + "", placeholder="e.g. bibliogram.art") + label(for="domain_quora") Replace Quora links with Quetre (blank to disable): + if(user_preferences.domain_quora != '' && typeof(user_preferences.domain_quora) != 'undefined') + input(type="text", name="domain_quora", id="domain_quora", value="" + user_preferences.domain_quora + "", placeholder="e.g. quetre.iket.me") else - input(type="text", name="domain_instagram", id="domain_instagram", placeholder="e.g. bibliogram.art") + input(type="text", name="domain_quora", id="domain_quora", placeholder="e.g. quetre.iket.me") + .setting + label(for="domain_imgur") Replace Imgur links with Rimgo (blank to disable): + if(user_preferences.domain_imgur != '' && typeof(user_preferences.domain_imgur) != 'undefined') + input(type="text", name="domain_imgur", id="domain_imgur", value="" + user_preferences.domain_imgur + "", placeholder="e.g. rimgo.bcow.xyz") + else + input(type="text", name="domain_imgur", id="domain_imgur", placeholder="e.g. rimgo.bcow.xyz") legend Display .setting label(for="theme") Theme: select(id="theme", name="theme") if(!user_preferences.theme || user_preferences.theme === 'auto') option(value="auto", selected="selected") Auto - option(value="") White + option(value="white") White option(value="dark") Dark option(value="sepia") Sepia - if(user_preferences.theme == '') + option(value="nord") Nord + else if(user_preferences.theme == 'white') option(value="auto") Auto - option(value="", selected="selected") White + option(value="white", selected="selected") White option(value="dark") Dark option(value="sepia") Sepia - if(user_preferences.theme === 'dark') + option(value="nord") Nord + else if(user_preferences.theme === 'dark') option(value="auto") Auto - option(value="") White + option(value="white") White option(value="dark", selected="selected") Dark option(value="sepia") Sepia - if(user_preferences.theme === 'sepia') + option(value="nord") Nord + else if(user_preferences.theme === 'sepia') option(value="auto") Auto - option(value="") White + option(value="white") White option(value="dark") Dark option(value="sepia", selected="selected") Sepia + option(value="nord") Nord + else if(user_preferences.theme === 'nord') + option(value="auto") Auto + option(value="white") White + option(value="dark") Dark + option(value="sepia") Sepia + option(value="nord", selected="selected") Nord + else + option(value="auto") Auto + option(value="white") White + option(value="dark") Dark + option(value="sepia") Sepia + option(value="nord") Nord .setting label(for="flairs") Show flairs: if(!user_preferences.flairs || user_preferences.flairs == 'true') @@ -96,6 +121,50 @@ html input(type="checkbox", name="show_upvoted_percentage", id="show_upvoted_percentage", checked="checked") else input(type="checkbox", name="show_upvoted_percentage", id="show_upvoted_percentage") + .setting + label(for="show_upvotes") Show upvotes: + if(!user_preferences.show_upvotes || user_preferences.show_upvotes == 'true') + input(type="checkbox", name="show_upvotes", id="show_upvotes", checked="checked") + else + input(type="checkbox", name="show_upvotes", id="show_upvotes") + .setting + label(for="prefer_frontpage") Prefer reddit-style frontpage as homepage: + if ((!instance_config.clean_homepage && !user_preferences.prefer_frontpage) || user_preferences.prefer_frontpage == 'true') + input(type="checkbox", name="prefer_frontpage", id="prefer_frontpage", checked="checked") + else + input(type="checkbox", name="prefer_frontpage", id="prefer_frontpage") + .setting + label(for="default_comment_sort") Default comment sorting: + select(id="default_comment_sort", name="default_comment_sort") + - + let default_comment_sort_html = '' + let user_default_sort = user_preferences.default_comment_sort || 'best' + + for(let key of comment_sort_values) { + default_comment_sort_html += `` + } + != default_comment_sort_html + legend Media + .setting + label(for="videos_muted") Mute videos by default: + - + let videos_muted = false + if(instance_config.videos_muted === true || user_preferences.videos_muted === 'true') { + videos_muted = true + } + if(user_preferences.videos_muted === 'false') { + videos_muted = false + } + if(videos_muted) + input(type="checkbox", name="videos_muted", id="videos_muted", checked="checked") + else + input(type="checkbox", name="videos_muted", id="videos_muted") + .setting + label(for="show_large_gallery_images") Show large gallery images with captions: + if (user_preferences.show_large_gallery_images == 'true') + input(type="checkbox", name="show_large_gallery_images", id="show_large_gallery_images", checked="checked") + else + input(type="checkbox", name="show_large_gallery_images", id="show_large_gallery_images") small(class="notice") Preferences are stored client-side using cookies without any personal information. br input(type="submit", value="Save preferences") diff --git a/views/privacypolicy.pug b/views/privacypolicy.pug index 20bf2f97..7ca4e85d 100644 --- a/views/privacypolicy.pug +++ b/views/privacypolicy.pug @@ -2,6 +2,9 @@ doctype html html head title privacy policy - teddit + meta(property='og:title', content='privacy policy - teddit') + include includes/meta_default.pug + include includes/meta_description.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug diff --git a/views/saved.pug b/views/saved.pug index d31f4b67..5c4c244a 100644 --- a/views/saved.pug +++ b/views/saved.pug @@ -2,6 +2,9 @@ doctype html html head title saved + meta(property='og:title', content='saved - teddit') + include includes/meta_default.pug + include includes/meta_description.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug diff --git a/views/search.pug b/views/search.pug index a3a07c8e..f93a5570 100644 --- a/views/search.pug +++ b/views/search.pug @@ -3,8 +3,12 @@ html head if no_query title search teddit + meta(property='og:title', content='search - teddit') else title search results for #{q} + meta(property='og:title', content='search results for ' + q + ' - teddit') + include includes/meta_default.pug + include includes/meta_description.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug @@ -137,57 +141,11 @@ html span.tag.nsfw NSFW a(href="/subreddits/search?q="+ q +"", class="btn") show more similar subreddits each link in json.posts - .link - .upvotes - .arrow - span #{kFormatter(link.ups)} - .arrow.down - .image - if link.images - if link.is_self_link - a(href="" + link.permalink + "") - img(src="" + link.images.thumb + "", alt="") - else - a(href=""+ link.url +"", rel="noopener noreferrer") - img(src="" + link.images.thumb + "", alt="") - else - a(href="" + link.permalink + "") - .no-image no image - .entry - .title - if link.is_self_link - a(href="" + link.permalink + "") - h2(class="" + (link.stickied ? 'green' : '') + "") #{cleanTitle(link.title)} - != link.link_flair - span (#{link.domain}) - else - a(href="" + link.url + "", rel="noopener noreferrer") - h2(class="" + (link.stickied ? 'green' : '') + "") #{cleanTitle(link.title)} - != link.link_flair - span (#{link.domain}) - .meta - p.submitted submitted - span(title="" + toUTCString(link.created) + "") #{timeDifference(link.created)} by - if link.author === '[deleted]' - span(class="deleted") [deleted] - else - a(href="/u/" + link.author + "") - | #{link.author} - != link.user_flair - p.to to - a(href="/r/" + link.subreddit + "") - | #{link.subreddit} - if link.stickied - span(class="green") stickied - .links - if link.over_18 - span.tag.nsfw NSFW - a(href="" + link.permalink + "", class="comments") - | #{link.num_comments} comments + include components/link.pug if json.before || json.after .view-more-links - if json.before && !subreddit_front - a(href="?q=" + q + "&restrict_sr=" + restrict_sr + "&nsfw=" + nsfw + "&before=" + json.before + "") ‹ prev + if json.before + a(href="?q=" + q + "&restrict_sr=" + restrict_sr + "&nsfw=" + nsfw + "&before=" + json.before + "" + (sortby === "new" && sortby ? "&" : "&sort=" + sortby + "&") + (!past ? "" : "t=" + past)) ‹ prev if json.after - a(href="?q=" + q + "&restrict_sr=" + restrict_sr + "&nsfw=" + nsfw + "&after=" + json.after + "") next › + a(href="?q=" + q + "&restrict_sr=" + restrict_sr + "&nsfw=" + nsfw + "&after=" + json.after + "" + (sortby === "new" && sortby ? "&" : "&sort=" + sortby + "&") + (!past ? "" : "t=" + past)) next › include includes/footer.pug diff --git a/views/subreddit.pug b/views/subreddit.pug index bfdbfe61..1d889fc5 100644 --- a/views/subreddit.pug +++ b/views/subreddit.pug @@ -2,6 +2,10 @@ doctype html html head title /r/#{subreddit} + meta(property='og:title', content='/r/' + subreddit) + if subreddit_about + meta(property='og:description', content='' + unescape(subreddit_about.public_description_html, user_preferences)) + include includes/meta_default.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug @@ -19,7 +23,8 @@ html if error if json.error_data.reason === "private" h2 This is a private subreddit. - p Error: #{JSON.stringify(json.error_data)} + if json.error_data + p Error: #{JSON.stringify(json.error_data)} else if show_nsfw_warning === true .nsfw-warning @@ -84,96 +89,7 @@ html p This subreddit either doesn't exist, or any posts weren't found. else each link in json.links - .link - .upvotes - .arrow - span #{kFormatter(link.ups)} - .arrow.down - .image - if link.images - if link.is_self_link - a(href="" + link.permalink + "") - img(src="" + link.images.thumb + "", alt="") - else - a(href=""+ link.url +"", rel="noopener noreferrer") - img(src="" + link.images.thumb + "", alt="") - else - a(href="" + link.permalink + "") - .no-image no image - .entry - .title - if link.is_self_link - a(href="" + link.permalink + "") - h2(class="" + (link.stickied ? 'green' : '') + "") #{cleanTitle(link.title)} - != link.link_flair - span (#{link.domain}) - else - a(href="" + link.url + "", rel="noopener noreferrer") - h2(class="" + (link.stickied ? 'green' : '') + "") #{cleanTitle(link.title)} - != link.link_flair - span (#{link.domain}) - .meta - p.submitted submitted - span(title="" + toUTCString(link.created) + "") #{timeDifference(link.created)} by - if link.author === '[deleted]' - span(class="deleted") [deleted] - else - a(href="/u/" + link.author + "") - | #{link.author} - != link.user_flair - p.to to - a(href="/r/" + link.subreddit + "") - | #{link.subreddit} - if link.stickied - span(class="green") stickied - .links - if link.over_18 - span.tag.nsfw NSFW - if link.selftext_html - details - summary - .line - .line - .line - .selftext - != unescape(link.selftext_html, user_preferences) - if (link.images && link.images.preview) - style. - details.preview-container img { - width: 100% !important; - height: auto !important; - max-width: none !important; - max-height: none !important; - opacity: 0; - } - details.preview-container[open][data-url="#{link.images.preview}"] .preview { - width: 100%; - height: auto; - background-image: url('#{link.images.preview}'); - background-repeat: no-repeat; - background-size: contain; - } - details.preview-container(data-url="" + link.images.preview + "") - summary - span ▶ - .preview - img(src=""+ link.images.thumb +"", alt="") - a(href="" + link.permalink + "", class="comments") #{link.num_comments} comments - - - let back_url = "/r/" + subreddit + "/" + sortby + "§2t="+ (past ? past : '') +"" - if(before && !subreddit_front) - back_url = "/r/" + subreddit + "/" + sortby + "§2t="+ (past ? past : '') +"§1before=" + before + "" - if(after) - back_url = "/r/" + subreddit + "/" + sortby + "§2t=" + (past ? past : '') + "§1after=" + after + "" - - let saved_post = false - if user_preferences.saved - each post_id in user_preferences.saved - if post_id === link.id - - saved_post = true - if saved_post - a(href="/unsave/" + link.id + "/?rk=" + redis_key + "&b=" + back_url + "") unsave - else - a(href="/save/" + link.id + "/?rk=" + redis_key + "&b=" + back_url + "") save + include components/link.pug if json.info.before || json.info.after .view-more-links if json.info.before && !subreddit_front diff --git a/views/subreddit_wiki.pug b/views/subreddit_wiki.pug index bd097108..46a158f3 100644 --- a/views/subreddit_wiki.pug +++ b/views/subreddit_wiki.pug @@ -2,6 +2,8 @@ doctype html html head title wiki /r/#{subreddit} + meta(property='og:title', content='wiki /r/' + subreddit) + include includes/meta_default.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug diff --git a/views/subreddits_explore.pug b/views/subreddits_explore.pug index 55fa309c..6d8a455c 100644 --- a/views/subreddits_explore.pug +++ b/views/subreddits_explore.pug @@ -2,6 +2,9 @@ doctype html html head title subreddits - explore + meta(property='og:title', content='explore subreddits - teddit') + include includes/meta_default.pug + include includes/meta_description.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug diff --git a/views/user.pug b/views/user.pug index 520f8260..9471e8ca 100644 --- a/views/user.pug +++ b/views/user.pug @@ -2,6 +2,8 @@ doctype html html head title overview for #{data.username} + meta(property='og:title', content='u/' + data.username + ' - teddit') + include includes/meta_default.pug include includes/head.pug body(class=""+ (user_preferences.theme === 'auto' ? 'dark' : user_preferences.theme) + "") include includes/topbar.pug @@ -77,39 +79,16 @@ html br p(title="" + toUTCString(data.created) + "") account created: #{toDateString(data.created)} p verified: #{(data.verified) ? "yes" : "no" } - .entries + #links.entries if !data.posts || data.posts.length <= 0 h3 no posts/comments each post in data.posts if post.type === 't3' - .entry.t3 - .upvotes - .arrow - span #{kFormatter(post.ups)} - .arrow.down - .image - if post.thumbnail !== 'self' - a(href="" + post.permalink + "", rel="noopener noreferrer") - img(src="" + post.thumbnail + "", alt="") - if post.duration - span #{secondsToMMSS(post.duration)} - else - a(href="" + post.permalink + "", rel="noopener noreferrer") - .no-image no image - .title - a(href="" + post.permalink + "", rel="noopener noreferrer") #{cleanTitle(post.title)} - .meta - p.submitted(title="" + toUTCString(post.created) + "") submitted #{timeDifference(post.created)} - | by - a(href="/u/" + data.username + "") #{data.username} - | to - != post.user_flair - a(href="/r/" + post.subreddit + "", class="subreddit") #{post.subreddit} - if post.over_18 - span.tag.nsfw NSFW - a.comments(href="" + post.permalink + "") #{post.num_comments} comments + - + var link = post; + include components/link.pug if post.type === 't1' - .entry + .commententry .meta .title a(href="" + post.url + "", rel="noopener noreferrer") #{cleanTitle(post.link_title)} @@ -144,7 +123,7 @@ html if data.before || data.after p view more: if data.before && !data.user_front - a(href="/u/" + data.username + data.post_type + "?before=" + data.before + "") ‹ prev + a(href="/u/" + data.username + data.post_type + (sortby === "new" ? "?" : "?sort=" + sortby + "&") + (!past ? "" : "t=" + past + "&") + "before=" + data.before + "") ‹ prev if data.after - a(href="/u/" + data.username + data.post_type + "?after=" + data.after + "") next › + a(href="/u/" + data.username + data.post_type + (sortby === "new" ? "?" : "?sort=" + sortby + "&") + (!past ? "" : "t=" + past + "&") + "after=" + data.after + "") next › include includes/footer.pug