├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── nightly.yml ├── .gitignore ├── .nvmrc ├── README.md ├── ansible ├── ansible-playbook.sh ├── ansible.cfg ├── ansible.sh ├── broker.yml ├── capture.yml ├── certificate.key ├── cluster.yml.old ├── commander.yml ├── destroy.yml ├── gateway.yml ├── inventory.yml ├── manager.yml ├── monolith.yml ├── roles │ ├── base │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── env.yml │ │ │ ├── firewall.yml │ │ │ ├── main.yml │ │ │ ├── screen.yml │ │ │ ├── tz.yml │ │ │ └── user.yml │ │ └── templates │ │ │ └── sshConfig.j2 │ ├── broker │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── aedes.service.j2 │ │ │ ├── config.js.j2 │ │ │ └── credentials.json.j2 │ ├── builder │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── main.yml │ │ │ └── provision.yml │ │ └── templates │ │ │ └── builder.service.j2 │ ├── capture │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── b2-linux.yml │ │ │ ├── caddy.yml │ │ │ ├── main.yml │ │ │ ├── provision.yml │ │ │ ├── rclone.yaml │ │ │ └── ytdl.yml │ │ └── templates │ │ │ ├── Caddyfile.j2 │ │ │ ├── capture.service.j2 │ │ │ └── rclone.conf.j2 │ ├── cluster │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── identity.json.j2 │ │ │ ├── ipfs-cluster.service.j2 │ │ │ ├── peerstore.j2 │ │ │ └── service.json.j2 │ ├── commander │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── caddy.yml │ │ │ ├── commander.yml │ │ │ ├── db.yml │ │ │ ├── main.yml │ │ │ ├── ufw.yml │ │ │ └── user.yml │ │ └── templates │ │ │ ├── Caddyfile.j2 │ │ │ └── commander.service.j2 │ ├── grafana │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── configure.yml │ │ │ ├── install.yml │ │ │ └── main.yml │ │ └── templates │ │ │ ├── grafana-agent.service.j2 │ │ │ └── grafana-agent.yml.j2 │ ├── ipfs-old │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── ipfs.service.j2 │ ├── ipfs │ │ ├── README.md │ │ ├── files │ │ │ └── index.html │ │ ├── handlers │ │ │ └── main.yml │ │ ├── patrons-only-auth-caddyfile │ │ ├── tasks │ │ │ ├── caddy.yml │ │ │ ├── certbot.yml │ │ │ ├── dns.yml │ │ │ ├── firewall.yml │ │ │ ├── ipfs.yml │ │ │ ├── main.yml │ │ │ ├── nginx.yml │ │ │ └── sysctl.yml │ │ └── templates │ │ │ ├── Caddyfile.j2 │ │ │ ├── caddy.service.j2 │ │ │ ├── certbot_luadns.ini.j2 │ │ │ ├── ipfs-config.json.j2 │ │ │ ├── ipfs.service.j2 │ │ │ ├── letsencrypt_cli.ini.j2 │ │ │ └── nginx.j2 │ ├── manager │ │ ├── README.md │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── sshConfig.j2 │ ├── prometheus │ │ └── tasks │ │ │ └── main.yml │ ├── qa │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── qa.service.j2 │ ├── requirements.yml │ ├── scout │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── scout.service.j2 │ ├── spinup │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ ├── main.yml │ │ │ ├── main.yml.old │ │ │ └── terraform.yml │ └── uppy │ │ ├── files │ │ ├── .gitignore │ │ ├── companion.js │ │ ├── package.json │ │ └── yarn.lock │ │ ├── handlers │ │ └── main.yml │ │ ├── tasks │ │ ├── caddy.yml │ │ ├── companion.yml │ │ ├── dns.yml │ │ ├── env.yml │ │ ├── firewall.yml │ │ ├── main.yml │ │ ├── redis.yml │ │ ├── screen.yml │ │ └── user.yml │ │ └── templates │ │ ├── Caddyfile.j2 │ │ ├── companion.service.j2 │ │ └── config.js.j2 ├── uppy.yml └── vultr.yml ├── futureporn.mjs ├── misc ├── futureporn-logo.xcf ├── futureporn-logo_256.png ├── futureporn-logo_64.png ├── patreon-streams.md └── projektmelody-torrents.md ├── package.json ├── packages ├── auth │ ├── .gitignore │ ├── index.js │ ├── package.json │ └── tunnel.conf ├── builder │ ├── .eleventy.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .nvmrc │ ├── .pnpmfile.cjs │ ├── _website │ │ ├── 404.njk │ │ ├── _data │ │ │ ├── .gitignore │ │ │ ├── .gitkeep │ │ │ ├── contributors.json │ │ │ ├── db.cjs │ │ │ ├── db.cjs.old │ │ │ ├── donors.json │ │ │ ├── env.cjs │ │ │ ├── gateways.cjs │ │ │ ├── github.cjs │ │ │ ├── metadata.json │ │ │ └── patreon.cjs │ │ ├── _includes │ │ │ ├── alpine │ │ │ │ ├── components │ │ │ │ │ ├── auth.js │ │ │ │ │ ├── player.js │ │ │ │ │ ├── tagger.js │ │ │ │ │ ├── upload.js │ │ │ │ │ └── user.js │ │ │ │ └── stores │ │ │ │ │ ├── auth.js │ │ │ │ │ ├── env.js │ │ │ │ │ ├── player.js │ │ │ │ │ └── user.js │ │ │ ├── fundingGoal.njk │ │ │ ├── heading.njk │ │ │ ├── js │ │ │ │ ├── base.js │ │ │ │ ├── common.js │ │ │ │ └── profile.js │ │ │ ├── layouts │ │ │ │ ├── base.njk │ │ │ │ ├── profile.njk │ │ │ │ ├── vod.njk │ │ │ │ └── vod2.njk │ │ │ ├── styles │ │ │ │ ├── base.scss │ │ │ │ ├── player.scss │ │ │ │ ├── tagger.css │ │ │ │ └── variables.scss │ │ │ ├── toyCard.njk │ │ │ ├── vodCard.njk │ │ │ └── vodslist.njk │ │ ├── about.njk │ │ ├── api │ │ │ ├── index.njk │ │ │ ├── service.njk │ │ │ └── v1.njk │ │ ├── connect │ │ │ └── patreon │ │ │ │ └── redirect.njk │ │ ├── faq.njk │ │ ├── favicon.njk │ │ ├── favicon │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon.ico │ │ │ └── favicon.png │ │ ├── feed │ │ │ ├── feed.xml.njk │ │ │ └── index.njk │ │ ├── goals.njk │ │ ├── index.njk │ │ ├── patrons.njk │ │ ├── profile.njk │ │ ├── sitemap.xml.njk │ │ ├── tags-list.njk │ │ ├── tags-pages.njk │ │ ├── testbed.njk │ │ ├── upload.njk │ │ ├── vod-pages.11ty.js │ │ └── vt │ │ │ └── projektmelody │ │ │ ├── index.njk │ │ │ ├── music.njk │ │ │ └── toy-list.md │ ├── index.js │ ├── migrations │ │ ├── 20230215-import-vods.js │ │ └── 20230218-update-240.js │ ├── netlify.toml │ ├── package.json │ ├── public │ │ ├── img │ │ │ ├── cj_clippy.jpeg │ │ │ ├── cj_clippy_sm.jpeg │ │ │ ├── gwfuturepornnet.png │ │ │ └── projekt-melody.jpg │ │ └── webfonts │ │ │ ├── fa-brands-400.ttf │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.ttf │ │ │ ├── fa-regular-400.woff2 │ │ │ ├── fa-solid-900.ttf │ │ │ ├── fa-solid-900.woff2 │ │ │ ├── fa-v4compatibility.ttf │ │ │ └── fa-v4compatibility.woff2 │ ├── removeUrlEncodedInputPaths.js │ └── vite.config.js ├── capture │ ├── .gitignore │ ├── README.md │ ├── index.js │ ├── integration │ │ ├── Capture.test.js │ │ ├── Ipfs.test.js │ │ ├── Voddo.test.js │ │ └── video.test.js │ ├── package.json │ ├── src │ │ ├── Capture.js │ │ ├── Ipfs.js │ │ ├── Video.js │ │ ├── Voddo.js │ │ ├── add.sh │ │ ├── cb.js │ │ └── record.js │ └── test │ │ ├── Capture.test.js │ │ ├── Video.test.js │ │ ├── Voddo.test.js │ │ ├── fixtures │ │ ├── just-a-text-file.txt │ │ ├── mock-stream0.mp4 │ │ ├── mock-stream1.mp4 │ │ └── mock-stream2.mp4 │ │ └── integration │ │ └── record.test.js ├── chat │ ├── .gitignore │ ├── index.js │ ├── package.json │ ├── readChat.js │ └── yarn.lock ├── commander │ ├── .gitignore │ ├── README.md │ ├── dev.js │ ├── index.js │ ├── package.json │ └── views │ │ ├── command.njk │ │ └── index.njk ├── common │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── Cluster.js │ │ ├── add.js │ │ ├── constants.js │ │ ├── dump.txt │ │ ├── dump_bak.txt │ │ ├── id.js │ │ ├── logger.js │ │ └── res.json │ └── test │ │ ├── fixtures │ │ └── screenshot.png │ │ └── integration │ │ └── ipfsCluster.test.js ├── render │ ├── .gitignore │ ├── index.js │ ├── node_modules │ │ ├── common │ │ ├── dotenv │ │ ├── fastq │ │ └── postgres │ ├── package.json │ └── test │ │ └── render.test.js ├── scout │ ├── .gitignore │ ├── .nvmrc │ ├── README.md │ ├── index.js │ ├── index.js.old │ ├── package.json │ ├── src │ │ ├── Room.js │ │ ├── chaturbate.js │ │ ├── constants.js │ │ ├── scrap.json │ │ ├── tweetProcess.js │ │ └── twitter.js │ ├── taco.html │ ├── test │ │ ├── integration │ │ │ ├── Room.test.js │ │ │ └── chaturbate.test.js │ │ └── unit │ │ │ └── tweetProcess.test.js │ └── testdex.js └── vibe │ ├── .gitignore │ ├── index.js │ ├── package.json │ ├── roboflow │ └── index.js │ ├── src │ └── roboflow.js │ ├── test │ └── roboflow.test.js │ └── yarn.lock ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── test ├── VOD.test.js ├── cid.test.js ├── cj_clippy_avatar.png ├── ipfsClusterUpload.test.js ├── testvid.mkv ├── testvid.mp4 └── twitchy-gimp.png └── utils ├── VOD.js ├── checklist.sh ├── cid.js ├── constants.js ├── export.js ├── getDateFromTwitter.js ├── goog-dl.sh ├── historian.js ├── imp.json ├── ipfsCluster.js ├── phdl ├── put-files-from-fs.js ├── roboflow.js ├── tweetBlacklist.js ├── tweetProcess.js ├── tweetprocess.test.js └── uploadProcess.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: trigger netlify build 13 | env: 14 | URL: ${{ secrets.NETLIFY_NIGHTLY_HOOK_URL }} 15 | run: | 16 | curl -X POST -d '{}' $URL 17 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/gallium 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # futureporn 2 | 3 | Unofficial ProjektMelody Chaturbate VOD Archive. For Adults Only. (NSFW) 4 | 5 | https://futureporn.net/ 6 | 7 | **DEPRECATED REPO** 8 | 9 | Moved to https://gitea.futureporn.net/ 10 | -------------------------------------------------------------------------------- /ansible/ansible-playbook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Loading variables from .env files in Ansible 4 | ## Greets https://gist.github.com/berkayunal/ccb1c3511f02d41b7654de17bced30b7 5 | 6 | set -o nounset -o pipefail -o errexit 7 | 8 | # Load all variables from .env and export them all for Ansible to read 9 | set -o allexport 10 | source "$(dirname "$0")/.env" 11 | set +o allexport 12 | 13 | # Run Ansible 14 | exec ansible-playbook "$@" 15 | 16 | -------------------------------------------------------------------------------- /ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | nocows = True 3 | serial = 1 4 | 5 | [inventory] 6 | enable_plugins = vultr, yaml 7 | inventory = ./vultr.yml -------------------------------------------------------------------------------- /ansible/ansible.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Loading variables from .env files in Ansible 4 | ## Greets https://gist.github.com/berkayunal/ccb1c3511f02d41b7654de17bced30b7 5 | 6 | set -o nounset -o pipefail -o errexit 7 | 8 | # Load all variables from .env and export them all for Ansible to read 9 | set -o allexport 10 | source "$(dirname "$0")/.env" 11 | set +o allexport 12 | 13 | # Run Ansible 14 | exec ansible "$@" 15 | 16 | -------------------------------------------------------------------------------- /ansible/broker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: provision the MQTT broker 5 | hosts: broker 6 | vars: 7 | REDIS_HOST: "{{ lookup('env', 'REDIS_HOST') }}" 8 | REDIS_PORT: "{{ lookup('env', 'REDIS_PORT') }}" 9 | REDIS_PASSWORD: "{{ lookup('env', 'REDIS_PASSWORD') }}" 10 | REDIS_USERNAME: "{{ lookup('env', 'REDIS_USERNAME') }}" 11 | AEDES_PASSWORD_HASH: "{{ lookup('env', 'AEDES_PASSWORD_HASH') }}" 12 | AEDES_PASSWORD_SALT: "{{ lookup('env', 'AEDES_PASSWORD_SALT') }}" 13 | ansible_user: root 14 | nodejs_version: "18.x" 15 | roles: 16 | - role: geerlingguy.nodejs 17 | - role: broker 18 | tags: [broker] -------------------------------------------------------------------------------- /ansible/capture.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: spin up instances 4 | hosts: localhost 5 | vars: 6 | - instance_plan: vc2-1c-2gb 7 | - instance_count: 1 8 | - instance_type: capture 9 | roles: 10 | - role: spinup 11 | 12 | 13 | - name: install futureporn capture suite 14 | hosts: ~futureporn-capture\d 15 | gather_facts: no 16 | vars: 17 | - ansible_user: root 18 | - nodejs_version: "16.x" 19 | - ipfs_version: v0.12.2 20 | - ipfs_private_key: "{{ lookup('env', 'IPFS_PRIVATE_KEY') }}" 21 | - ipfs_peer_id: "{{ lookup('env', 'IPFS_PEER_ID') }}" 22 | - nodejs_version: "16.x" 23 | - storage_gb: 100 # minimum 40GB 24 | - ipfs_storage_max: "100GB" 25 | roles: 26 | - role: base 27 | - role: geerlingguy.nodejs 28 | 29 | - role: capture 30 | tags: [capture] 31 | 32 | - role: grafana 33 | tags: [grafana] 34 | 35 | - role: jeffbr13.ipfs 36 | 37 | # important that manager comes after jeffbr13.ipfs 38 | # as we monkey patch the ipfs.service file 39 | # - role: manager 40 | 41 | 42 | # - name: install futureporn-scout just on one VPS 43 | # hosts: futureporn-capture1 44 | # gather_facts: no # just for speed 45 | # vars: 46 | # - ansible_user: root 47 | # roles: 48 | # - role: scout 49 | # tags: [scout] -------------------------------------------------------------------------------- /ansible/commander.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | 5 | - name: provision commanders 6 | hosts: commanders 7 | gather_facts: no 8 | pre_tasks: 9 | - name: gather facts only if specific tag is used 10 | gather_facts: 11 | tags: 12 | - never 13 | - commander 14 | roles: 15 | - role: base 16 | tags: [base] 17 | - role: geerlingguy.nodejs 18 | tags: 19 | - commander 20 | - role: commander 21 | tags: [commander] 22 | 23 | -------------------------------------------------------------------------------- /ansible/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | tasks: 5 | - name: Ensure futureporn-capture is spun down 6 | ngine_io.vultr.vultr_server: 7 | name: futureporn-capture 8 | state: absent 9 | 10 | - name: Ensure futureporn-manager is spun down 11 | ngine_io.vultr.vultr_server: 12 | name: futureporn-manager 13 | state: absent -------------------------------------------------------------------------------- /ansible/gateway.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: spin up instances 4 | hosts: localhost 5 | vars: 6 | - instance_plan: vc2-1c-2gb 7 | - instance_count: 1 8 | - instance_type: gateway 9 | roles: 10 | - role: spinup 11 | 12 | 13 | - hosts: ~futureporn-gateway\d 14 | gather_facts: no 15 | vars: 16 | - ansible_user: root 17 | - ipfs_kubo_version: v0.17.0 18 | - ipfs_storage_max: 768GB 19 | roles: 20 | - role: base 21 | - role: nvjacobo.caddy 22 | - role: gateway 23 | -------------------------------------------------------------------------------- /ansible/manager.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | gather_facts: no 5 | vars: 6 | - vps_plan: vc2-1c-2gb 7 | - vps_region: atl 8 | - vps_hostname: futureporn-manager 9 | roles: 10 | - role: spinup 11 | 12 | 13 | - hosts: futureporn-manager 14 | gather_facts: no 15 | vars: 16 | - ansible_user: root 17 | - ipfs_version: v0.12.2 18 | - ipfs_private_key: "{{ lookup('env', 'IPFS_PRIVATE_KEY') }}" 19 | - ipfs_peer_id: "{{ lookup('env', 'IPFS_PEER_ID') }}" 20 | - nodejs_version: "16.x" 21 | - storage_gb: 500 22 | - ipfs_storage_max: "15GB" 23 | roles: 24 | - role: base 25 | - role: geerlingguy.nodejs 26 | - role: jeffbr13.ipfs 27 | - role: capture 28 | 29 | # important that this comes after jeffbr13.ipfs as we monkey patch the ipfs.service file 30 | - role: manager 31 | -------------------------------------------------------------------------------- /ansible/monolith.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: cluster monolith for a dedicated server 4 | hosts: monoliths 5 | gather_facts: no # `no` is a requirement for insanity54/base role. the base role gather facts once able. 6 | vars: 7 | - ansible_user: root 8 | - storage_gb: 3000 9 | - ipfs_gc_period: 3h 10 | - ipfs_cluster_service_version: v1.0.5 11 | - ipfs_cluster_service_checksum: sha256:842be4c780848e8d744ff0cdb410b3f88024af63f02d3d41cc040905fbcc6f8f 12 | # - certbot_admin_email: "{{ lookup('env', 'LUA_DNS_EMAIL') }}" 13 | # - certbot_create_command: "certbot certonly --noninteractive --dns-luadns --dns-luadns-credentials /etc/letsencrypt/luadns.ini --agree-tos --email {{ cert_item.email | default(certbot_admin_email) }} -d {{ cert_item.domains | join(',') }}" 14 | # - certbot_create_standalone_stop_services: [nginx] 15 | # - certbot_create_if_missing: yes 16 | # - certbot_certs: 17 | # - domains: 18 | # - "*.futureporn.net" 19 | pre_tasks: 20 | - name: gather facts only if specific tag is used 21 | gather_facts: 22 | tags: 23 | - never 24 | - gateway 25 | - cluster 26 | - scout 27 | - capture 28 | - grafana 29 | roles: 30 | - role: base 31 | tags: [base] 32 | - role: ipfs # also installs/configures IPFS, caddy 33 | tags: [ipfs] 34 | - role: cluster 35 | tags: [cluster] 36 | - role: geerlingguy.nodejs 37 | tags: 38 | - capture 39 | - scout 40 | - role: capture 41 | tags: [capture] 42 | - role: scout 43 | tags: [scout] 44 | - role: qa 45 | tags: [qa] 46 | - role: grafana 47 | tags: [grafana] 48 | -------------------------------------------------------------------------------- /ansible/roles/base/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart systemd-journald 4 | ansible.builtin.systemd: 5 | name: systemd-journald 6 | state: restarted 7 | -------------------------------------------------------------------------------- /ansible/roles/base/tasks/env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: Set B2_APPLICATION_KEY in env 5 | ansible.builtin.lineinfile: 6 | dest: /root/.bashrc 7 | line: "export B2_APPLICATION_KEY={{ lookup('env', 'B2_APPLICATION_KEY') }}" 8 | state: present 9 | 10 | - name: Set B2_APPLICATION_KEY_ID in env 11 | ansible.builtin.lineinfile: 12 | dest: /root/.bashrc 13 | line: "export B2_APPLICATION_KEY_ID={{ lookup('env', 'B2_APPLICATION_KEY_ID') }}" 14 | state: present 15 | 16 | - name: Set WEB3_TOKEN secret in env 17 | ansible.builtin.lineinfile: 18 | dest: /root/.bashrc 19 | line: "export WEB3_TOKEN={{ lookup('env', 'WEB3_TOKEN') }}" 20 | state: present 21 | 22 | - name: Set TWITTER_API_KEY in env 23 | ansible.builtin.lineinfile: 24 | dest: /root/.bashrc 25 | line: "export TWITTER_API_KEY={{ lookup('env', 'TWITTER_API_KEY') }}" 26 | state: present 27 | 28 | - name: Set TWITTER_API_KEY_SECRET in env 29 | ansible.builtin.lineinfile: 30 | dest: /root/.bashrc 31 | line: "export TWITTER_API_KEY_SECRET={{ lookup('env', 'TWITTER_API_KEY_SECRET') }}" 32 | state: present 33 | 34 | - name: Set IPFS CLUSTER HTTP API secrets in env 35 | ansible.builtin.lineinfile: 36 | dest: /root/.bashrc 37 | line: "export {{ item.key }}={{ item.value }}" 38 | state: present 39 | with_items: 40 | - { key: 'IPFS_CLUSTER_HTTP_API_USERNAME', value: "{{ lookup('env', 'IPFS_CLUSTER_HTTP_API_USERNAME') }}"} 41 | - { key: 'IPFS_CLUSTER_HTTP_API_PASSWORD', value: "\"{{ lookup('env', 'IPFS_CLUSTER_HTTP_API_PASSWORD') }}\""} 42 | - { key: 'IPFS_CLUSTER_HTTP_API_MULTIADDR', value: "{{ lookup('env', 'IPFS_CLUSTER_HTTP_API_MULTIADDR') }}" } 43 | tags: 44 | - clusterkey 45 | 46 | - name: Set FUTUREPORN_WORKDIR env var 47 | ansible.builtin.lineinfile: 48 | dest: /root/.bashrc 49 | line: "export FUTUREPORN_WORKDIR=/opt/futureporn_tmp" 50 | state: present 51 | 52 | - name: Set IPFS_PATH env var 53 | ansible.builtin.lineinfile: 54 | dest: /root/.bashrc 55 | line: "export IPFS_PATH=/home/ipfs/.ipfs" 56 | state: present 57 | 58 | - name: Set IPFS_CLUSTER_HTTP_API_USERNAME env var 59 | ansible.builtin.lineinfile: 60 | dest: /root/.bashrc 61 | line: "export IPFS_CLUSTER_HTTP_API_USERNAME={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_USERNAME') }}" 62 | state: present -------------------------------------------------------------------------------- /ansible/roles/base/tasks/firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Allow everything and enable UFW 4 | community.general.ufw: 5 | state: enabled 6 | policy: allow 7 | 8 | 9 | - name: Set logging 10 | community.general.ufw: 11 | logging: 'on' 12 | 13 | 14 | - name: create ufw exception for mosh 15 | community.general.ufw: 16 | rule: allow 17 | port: 60000:61000 18 | proto: udp 19 | 20 | -------------------------------------------------------------------------------- /ansible/roles/base/tasks/screen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Download screen configuration 4 | git: 5 | repo: 'https://github.com/insanity54/dotfiles' 6 | dest: /root/dotfiles 7 | 8 | - name: Configure screen 9 | copy: 10 | src: /root/dotfiles/.screenrc 11 | dest: /root/.screenrc 12 | remote_src: yes -------------------------------------------------------------------------------- /ansible/roles/base/tasks/tz.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Set timezone to UTC 4 | community.general.timezone: 5 | name: Etc/UTC -------------------------------------------------------------------------------- /ansible/roles/base/tasks/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: create futureporn group 4 | ansible.builtin.group: 5 | name: futureporn 6 | 7 | - name: create futureporn user 8 | ansible.builtin.user: 9 | name: futureporn 10 | group: futureporn 11 | groups: futureporn 12 | create_home: true 13 | home: /home/futureporn 14 | 15 | 16 | # # greets https://stackoverflow.com/a/68445912/1004931 17 | # - name: remove futureporn user from users group 18 | # become: true 19 | # command: "gpasswd -d futureporn users" 20 | # register: command_result 21 | # changed_when: "not 'is not a member of' in command_result.stderr" 22 | # failed_when: false 23 | -------------------------------------------------------------------------------- /ansible/roles/base/templates/sshConfig.j2: -------------------------------------------------------------------------------- 1 | Host github.com 2 | User git 3 | Port 22 4 | Hostname github.com 5 | IdentityFile ~/.ssh/futureporn 6 | TCPKeepAlive yes 7 | IdentitiesOnly yes 8 | StrictHostKeyChecking no 9 | -------------------------------------------------------------------------------- /ansible/roles/broker/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart aedes 4 | ansible.builtin.systemd: 5 | name: aedes 6 | state: restarted 7 | daemon_reload: true -------------------------------------------------------------------------------- /ansible/roles/broker/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: create aedes user 4 | user: 5 | name: aedes 6 | 7 | - name: create aedes group 8 | group: 9 | name: aedes 10 | 11 | - name: copy config to server 12 | template: 13 | src: config.js.j2 14 | dest: /home/aedes/aedes/config.js 15 | notify: 16 | - restart aedes 17 | 18 | - name: create credentials.json file 19 | template: 20 | src: credentials.json.j2 21 | dest: /home/aedes/aedes/credentials.json 22 | notify: 23 | - restart aedes 24 | 25 | - name: set config perms 26 | file: 27 | path: /home/aedes/aedes/config.js 28 | owner: aedes 29 | group: aedes 30 | mode: '0755' 31 | 32 | - name: set credentials perms 33 | file: 34 | path: /home/aedes/aedes/config.js 35 | owner: aedes 36 | group: aedes 37 | mode: '0600' 38 | 39 | - name: Install aedes-cli 40 | community.general.npm: 41 | name: aedes-cli 42 | version: '^0.6.0' 43 | path: /home/aedes/aedes 44 | 45 | - name: install aedes.service 46 | ansible.builtin.template: 47 | src: aedes.service.j2 48 | dest: /etc/systemd/system/aedes.service 49 | owner: root 50 | group: root 51 | mode: '0755' 52 | notify: 53 | - restart aedes -------------------------------------------------------------------------------- /ansible/roles/broker/templates/aedes.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description=aedes 6 | After=network.target 7 | 8 | [Service] 9 | Type=simple 10 | ExecStart=npm exec -- aedes --config /home/aedes/aedes/config.js 11 | User=aedes 12 | Group=aedes 13 | WorkingDirectory=/home/aedes/aedes 14 | Restart=always 15 | RestartSec=5 16 | Environment=PATH=/usr/bin:/usr/local/bin 17 | Environment=NODE_ENV=production 18 | -------------------------------------------------------------------------------- /ansible/roles/broker/templates/config.js.j2: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protos: ['tcp'], 3 | host: '0.0.0.0', 4 | port: 1883, 5 | wsPort: 3000, 6 | wssPort: 4000, 7 | tlsPort: 8883, 8 | brokerId: "{{ ansible_hostname }}", 9 | credentials: '/home/aedes/aedes/credentials.json', 10 | persistence: { 11 | name: 'redis', 12 | options: { 13 | host: '{{ REDIS_HOST }}', 14 | port: '{{ REDIS_PORT }}', 15 | password: '{{ REDIS_PASSWORD }}', 16 | username: '{{ REDIS_USERNAME }}', 17 | enableReadyCheck: false 18 | } 19 | }, 20 | mq: { 21 | name: 'redis', 22 | options: { 23 | host: '{{ REDIS_HOST }}', 24 | port: '{{ REDIS_PORT }}', 25 | password: '{{ REDIS_PASSWORD }}', 26 | username: '{{ REDIS_USERNAME }}', 27 | enableReadyCheck: false 28 | } 29 | }, 30 | stats: false, 31 | key: null, 32 | cert: null, 33 | rejectUnauthorized: true, 34 | verbose: true, 35 | veryVerbose: false, 36 | noPretty: false 37 | } -------------------------------------------------------------------------------- /ansible/roles/broker/templates/credentials.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "futureporn": { 3 | "salt": "{{ AEDES_PASSWORD_SALT }}", 4 | "hash": "{{ AEDES_PASSWORD_HASH }}", 5 | "authorizePublish": "futureporn/**", 6 | "authorizeSubscribe": "futureporn/**" 7 | } 8 | } -------------------------------------------------------------------------------- /ansible/roles/builder/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | -------------------------------------------------------------------------------- /ansible/roles/builder/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: restart builder 5 | ansible.builtin.systemd: 6 | name: builder 7 | state: restarted 8 | -------------------------------------------------------------------------------- /ansible/roles/builder/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - include_tasks: ./provision.yml -------------------------------------------------------------------------------- /ansible/roles/builder/tasks/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: create futureporn group 4 | ansible.builtin.group: 5 | name: futureporn 6 | 7 | - name: create futureporn user 8 | ansible.builtin.user: 9 | name: futureporn 10 | groups: futureporn 11 | group: futureporn 12 | create_home: true 13 | home: /home/futureporn 14 | 15 | - name: upload/install ssh deploy key 16 | copy: 17 | remote_src: false 18 | path: /home/chris/.ssh/futureporn-builder 19 | dest: /home/futureporn/futureporn/.ssh/futureporn-builder 20 | mode: '0600' 21 | become: yes 22 | become_user: futureporn 23 | 24 | - name: install yarn 25 | community.general.npm: 26 | name: yarn 27 | global: yes 28 | state: present 29 | 30 | - name: Download futureporn 31 | git: 32 | repo: https://github.com/insanity54/futureporn 33 | dest: /home/futureporn/futureporn 34 | depth: 1 35 | update: yes 36 | force: yes 37 | notify: 38 | - restart builder 39 | become: yes 40 | become_user: futureporn 41 | 42 | - name: Install futureporn 43 | community.general.yarn: 44 | path: /home/futureporn/futureporn/packages/builder 45 | notify: 46 | - restart builder 47 | become: yes 48 | become_user: futureporn 49 | 50 | - name: Transfer dev cache to build server 51 | copy: 52 | remote_src: false 53 | src: /home/chris/Documents/futureporn/packages/builder/.cache 54 | dest: /home/futureporn/futureporn/packages/builder 55 | 56 | - name: Install builder system service 57 | ansible.builtin.template: 58 | src: templates/builder.service.j2 59 | dest: /etc/systemd/system/builder.service 60 | owner: root 61 | group: root 62 | mode: '0755' 63 | notify: 64 | - restart builder 65 | 66 | - name: Enable builder system service 67 | systemd: 68 | name: builder 69 | state: started 70 | daemon_reload: yes 71 | enabled: yes 72 | notify: 73 | - restart builder 74 | 75 | -------------------------------------------------------------------------------- /ansible/roles/builder/templates/builder.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description="futureporn builder" 6 | After=network.target 7 | 8 | [Service] 9 | ExecStart=/usr/bin/env node /home/futureporn/futureporn/packages/builder/index.js 10 | WorkingDirectory=/home/futureporn/futureporn/packages/builder 11 | Type=simple 12 | Restart=always 13 | RestartSec=5 14 | Environment=POSTGRES_HOST={{ lookup('env', 'POSTGRES_HOST') }} 15 | Environment=POSTGRES_PASSWORD={{ lookup('env', 'POSTGRES_PASSWORD') }} 16 | Environment=POSTGRES_USERNAME={{ lookup('env', 'POSTGRES_USERNAME') }} 17 | Environment=FUTUREPORN_WORKDIR={{ lookup('env', 'FUTUREPORN_WORKDIR') }} 18 | Environment=NODE_ENV="production" 19 | -------------------------------------------------------------------------------- /ansible/roles/capture/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | task: provision 4 | workdir: /opt/futureporn_tmp -------------------------------------------------------------------------------- /ansible/roles/capture/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: restart capture 5 | ansible.builtin.systemd: 6 | name: capture 7 | state: restarted 8 | 9 | - name: restart capture-monitor 10 | ansible.builtin.systemd: 11 | name: capture-monitor 12 | state: restarted 13 | -------------------------------------------------------------------------------- /ansible/roles/capture/tasks/b2-linux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Set B2_APPLICATION_KEY_ID secret in env 4 | ansible.builtin.lineinfile: 5 | dest: /root/.bashrc 6 | line: "export B2_APPLICATION_KEY_ID={{ lookup('env', 'B2_APPLICATION_KEY_ID') }}" 7 | state: present 8 | 9 | - name: Set B2_APPLICATION_KEY secret in env 10 | ansible.builtin.lineinfile: 11 | dest: /root/.bashrc 12 | line: "export B2_APPLICATION_KEY={{ lookup('env', 'B2_APPLICATION_KEY') }}" 13 | state: present 14 | 15 | 16 | - name: Install b2-linux 17 | get_url: 18 | url: https://github.com/Backblaze/B2_Command_Line_Tool/releases/latest/download/b2-linux 19 | dest: /usr/local/bin/b2-linux 20 | owner: root 21 | group: root 22 | mode: '0755' -------------------------------------------------------------------------------- /ansible/roles/capture/tasks/caddy.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/ansible/roles/capture/tasks/caddy.yml -------------------------------------------------------------------------------- /ansible/roles/capture/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - include_tasks: ./ytdl.yml 5 | - include_tasks: ./b2-linux.yml 6 | - include_tasks: ./provision.yml -------------------------------------------------------------------------------- /ansible/roles/capture/tasks/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | 5 | - name: create folder for recordings 6 | file: 7 | path: "{{ lookup('env', 'FUTUREPORN_WORKDIR') }}/recordings" 8 | state: directory 9 | mode: '0755' 10 | 11 | - name: install yarn 12 | community.general.npm: 13 | name: yarn 14 | global: yes 15 | state: present 16 | 17 | - name: Install & update youtube-dl 18 | pip: 19 | name: youtube-dl 20 | state: latest 21 | 22 | 23 | - name: Download futureporn 24 | git: 25 | repo: https://github.com/insanity54/futureporn 26 | dest: /home/futureporn/futureporn 27 | depth: 1 28 | update: yes 29 | force: yes 30 | notify: 31 | - restart capture 32 | 33 | # not needed using yarn berry 34 | # - name: Install futureporn 35 | # community.general.yarn: 36 | # path: /root/futureporn/packages/capture 37 | # notify: 38 | # - restart capture 39 | 40 | - name: Install capture system service 41 | ansible.builtin.template: 42 | src: templates/capture.service.j2 43 | dest: /etc/systemd/system/capture.service 44 | owner: root 45 | group: root 46 | mode: '0755' 47 | notify: 48 | - restart capture 49 | 50 | - name: Enable capture system service 51 | systemd: 52 | name: capture 53 | state: started 54 | daemon_reload: yes 55 | enabled: yes 56 | notify: 57 | - restart capture 58 | 59 | - name: Download ipfs-cluster-ctl 60 | ansible.builtin.unarchive: 61 | remote_src: yes 62 | src: https://dist.ipfs.tech/ipfs-cluster-ctl/v1.0.2/ipfs-cluster-ctl_v1.0.2_linux-amd64.tar.gz 63 | dest: /root/ 64 | 65 | 66 | # futureporn.mjs needs this to upload files to the ipfs cluster 67 | - name: Install ipfs-cluster-ctl 68 | ansible.builtin.copy: 69 | remote_src: yes 70 | src: /root/ipfs-cluster-ctl/ipfs-cluster-ctl 71 | dest: /usr/local/bin/ipfs-cluster-ctl 72 | mode: '0755' 73 | -------------------------------------------------------------------------------- /ansible/roles/capture/tasks/rclone.yaml: -------------------------------------------------------------------------------- 1 | 2 | # rclone is deprecated in favor of b2-linux 3 | # due to Backblaze behaving poorly with rclone 4 | # im keeping this file here for reference 5 | 6 | - name: Install latest rclone 7 | apt: 8 | deb: https://downloads.rclone.org/rclone-current-linux-amd64.deb 9 | state: present 10 | # this is necessary because the apt version has a B2 upload bug 11 | 12 | - name: Create rclone conf directory 13 | ansible.builtin.file: 14 | state: directory 15 | path: /root/.config/rclone 16 | mode: '0755' 17 | 18 | - name: Configure rclone 19 | ansible.builtin.template: 20 | src: templates/rclone.conf.j2 21 | dest: /root/.config/rclone/rclone.conf 22 | owner: root 23 | group: root 24 | mode: '0600' -------------------------------------------------------------------------------- /ansible/roles/capture/tasks/ytdl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: install deps 5 | apt: 6 | pkg: 7 | - virtualenv 8 | state: present 9 | 10 | 11 | - name: Install & update youtube-dl 12 | pip: 13 | name: youtube-dl 14 | state: latest 15 | virtualenv: "{{ lookup('env', 'FUTUREPORN_WORKDIR') }}" 16 | notify: 17 | - restart capture -------------------------------------------------------------------------------- /ansible/roles/capture/templates/Caddyfile.j2: -------------------------------------------------------------------------------- 1 | 2 | reverse_proxy 127.0.0.1:3000 3 | -------------------------------------------------------------------------------- /ansible/roles/capture/templates/capture.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description="futureporn capture-monitor" 6 | After=network.target 7 | 8 | [Service] 9 | ExecStart=/usr/bin/env yarn node index 10 | WorkingDirectory=/home/futureporn/futureporn/packages/capture 11 | Restart=always 12 | RestartSec=5 13 | User=futureporn 14 | Group=futureporn 15 | Environment=PATH=/usr/bin:/usr/local/bin 16 | Environment=NODE_ENV=production 17 | Environment=POSTGRES_HOST={{ lookup('env', 'POSTGRES_HOST') }} 18 | Environment=POSTGRES_PASSWORD={{ lookup('env', 'POSTGRES_PASSWORD') }} 19 | Environment=POSTGRES_USERNAME={{ lookup('env', 'POSTGRES_USERNAME') }} 20 | Environment=FUTUREPORN_WORKDIR={{ lookup('env', 'FUTUREPORN_WORKDIR') }} 21 | Environment=IPFS_CLUSTER_HTTP_API_MULTIADDR={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_MULTIADDR') }} 22 | Environment=IPFS_CLUSTER_HTTP_API_USERNAME={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_USERNAME') }} 23 | Environment=IPFS_CLUSTER_HTTP_API_PASSWORD={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_PASSWORD') }} 24 | Environment=YOUTUBE_DL_BINARY={{ lookup('env', 'FUTUREPORN_WORKDIR') }}/bin/youtube-dl -------------------------------------------------------------------------------- /ansible/roles/capture/templates/rclone.conf.j2: -------------------------------------------------------------------------------- 1 | [b2] 2 | type = b2 3 | account = {{ lookup('env', 'B2_APPLICATION_KEY_ID') }} 4 | key = {{ lookup('env', 'B2_APPLICATION_KEY') }} 5 | 6 | -------------------------------------------------------------------------------- /ansible/roles/cluster/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: restart ipfs-cluster 5 | ansible.builtin.systemd: 6 | name: ipfs-cluster 7 | state: restarted 8 | -------------------------------------------------------------------------------- /ansible/roles/cluster/templates/identity.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "id": "{{ ipfs_cluster_id }}", 3 | "private_key": "{{ ipfs_cluster_privkey }}" 4 | } -------------------------------------------------------------------------------- /ansible/roles/cluster/templates/ipfs-cluster.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=IPFS Cluster Service 3 | After=network.target 4 | 5 | [Service] 6 | LimitNOFILE=10000 7 | Environment="IPFS_CLUSTER_FD_MAX=10000" 8 | ExecStart=/usr/local/bin/ipfs-cluster-service daemon 9 | Restart=always 10 | RestartSec=30 11 | User=ipfs 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ansible/roles/cluster/templates/peerstore.j2: -------------------------------------------------------------------------------- 1 | {% for peer in cluster_peers %} 2 | {{ peer }} 3 | {% endfor %} -------------------------------------------------------------------------------- /ansible/roles/commander/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | workdir: /opt/futureporn_tmp -------------------------------------------------------------------------------- /ansible/roles/commander/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: restart commander 5 | ansible.builtin.systemd: 6 | name: commander 7 | state: restarted 8 | 9 | 10 | - name: reload postgresql 11 | ansible.builtin.systemd: 12 | name: postgresql 13 | state: reloaded 14 | 15 | 16 | - name: reload caddy 17 | ansible.builtin.systemd: 18 | name: caddy 19 | state: reloaded 20 | daemon_reload: yes 21 | -------------------------------------------------------------------------------- /ansible/roles/commander/tasks/caddy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: install certutil 5 | apt: 6 | package: libnss3-tools 7 | state: present 8 | 9 | - name: install caddy 10 | include_role: 11 | name: nvjacobo.caddy 12 | 13 | 14 | - name: Install Caddyfile 15 | ansible.builtin.template: 16 | src: templates/Caddyfile.j2 17 | dest: /etc/caddy/Caddyfile 18 | owner: root 19 | group: root 20 | mode: '0755' 21 | notify: 22 | - restart caddy -------------------------------------------------------------------------------- /ansible/roles/commander/tasks/commander.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | 5 | 6 | - name: install yarn 7 | community.general.npm: 8 | name: yarn 9 | global: yes 10 | state: present 11 | 12 | - name: Download futureporn 13 | git: 14 | repo: https://github.com/insanity54/futureporn 15 | dest: /home/futureporn/futureporn 16 | depth: 1 17 | update: yes 18 | force: yes 19 | notify: 20 | - restart commander 21 | become: yes 22 | become_user: futureporn 23 | 24 | # not needed because of berry zero-install 25 | # - name: Install futureporn commander 26 | # command: 27 | # yarn install 28 | # community.general.yarn: 29 | # path: /home/futureporn/futureporn/packages/commander 30 | # production: yes 31 | # notify: 32 | # - restart commander 33 | # become: yes 34 | # become_user: futureporn 35 | 36 | - name: Install commander system service 37 | ansible.builtin.template: 38 | src: templates/commander.service.j2 39 | dest: /etc/systemd/system/commander.service 40 | owner: root 41 | group: root 42 | mode: '0755' 43 | notify: 44 | - restart commander 45 | 46 | - name: Enable commander system service 47 | systemd: 48 | name: commander 49 | state: started 50 | daemon_reload: yes 51 | enabled: yes 52 | notify: 53 | - restart commander 54 | 55 | 56 | -------------------------------------------------------------------------------- /ansible/roles/commander/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - include_tasks: ./ufw.yml 5 | - include_tasks: ./user.yml 6 | - include_tasks: ./db.yml 7 | - include_tasks: ./caddy.yml 8 | - include_tasks: ./commander.yml -------------------------------------------------------------------------------- /ansible/roles/commander/tasks/ufw.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: Allow everything and enable UFW 5 | community.general.ufw: 6 | state: enabled 7 | policy: allow 8 | 9 | 10 | 11 | - name: Disallow direct app access 12 | community.general.ufw: 13 | rule: reject 14 | port: '8080' 15 | 16 | 17 | - name: Allow reverse proxy access 18 | community.general.ufw: 19 | rule: allow 20 | port: '80' 21 | 22 | 23 | - name: Allow postgres port 24 | community.general.ufw: 25 | rule: allow 26 | port: '5432' -------------------------------------------------------------------------------- /ansible/roles/commander/tasks/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: create futureporn group 4 | ansible.builtin.group: 5 | name: futureporn 6 | 7 | - name: create futureporn user 8 | ansible.builtin.user: 9 | name: futureporn 10 | group: futureporn 11 | groups: futureporn 12 | create_home: true 13 | home: /home/futureporn 14 | -------------------------------------------------------------------------------- /ansible/roles/commander/templates/Caddyfile.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | commander.sbtp.xyz { 4 | 5 | tls chris@grimtech.net 6 | 7 | basicauth / { 8 | {{ lookup('env', 'COMMANDER_USERNAME' ) }} {{ lookup('env', 'COMMANDER_PASSWORD') | password_hash('bcrypt') }} 9 | } 10 | 11 | reverse_proxy 127.0.0.1:8080 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ansible/roles/commander/templates/commander.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description="futureporn commander" 6 | After=network.target 7 | 8 | [Service] 9 | ExecStart=/usr/bin/env yarn node index 10 | WorkingDirectory=/home/futureporn/futureporn/packages/commander 11 | Type=simple 12 | Restart=always 13 | RestartSec=5 14 | Environment=POSTGRES_HOST=localhost 15 | Environment=POSTGRES_PASSWORD={{ lookup('env', 'POSTGRES_PASSWORD') }} 16 | Environment=POSTGRES_USERNAME={{ lookup('env', 'POSTGRES_USERNAME') }} 17 | Environment=FUTUREPORN_WORKDIR={{ lookup('env', 'FUTUREPORN_WORKDIR') }} 18 | Environment=COMMANDER_USERNAME={{ lookup('env', 'COMMANDER_USERNAME') }} 19 | Environment=COMMANDER_PASSWORD={{ lookup('env', 'COMMANDER_PASSWORD') }} 20 | Environment=PORT=8080 -------------------------------------------------------------------------------- /ansible/roles/grafana/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart grafana-agent 4 | systemd: 5 | daemon_reload: yes 6 | name: grafana-agent 7 | enabled: yes 8 | state: restarted -------------------------------------------------------------------------------- /ansible/roles/grafana/tasks/configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - template: 4 | src: grafana-agent.yml.j2 5 | dest: "{{ agent_config_location }}/grafana-agent.yml" 6 | notify: restart grafana-agent -------------------------------------------------------------------------------- /ansible/roles/grafana/tasks/install.yml: -------------------------------------------------------------------------------- 1 | 2 | # greets https://grafana.com/docs/grafana-cloud/infrastructure-as-code/ansible/ansible-grafana-agent-linux/ 3 | 4 | - name: add group 'grafana-agent' 5 | ansible.builtin.group: 6 | name: grafana-agent 7 | 8 | - name: Add user 'grafana-agent' 9 | user: 10 | name: grafana-agent 11 | groups: grafana-agent 12 | create_home: true 13 | home: /home/grafana-agent 14 | shell: /bin/false 15 | 16 | - name: create cache dir 17 | file: 18 | state: directory 19 | path: /home/grafana-agent/.cache/ 20 | owner: grafana-agent 21 | group: grafana-agent 22 | 23 | - name: create positions dir 24 | file: 25 | state: directory 26 | path: /home/grafana-agent/positions/ 27 | owner: grafana-agent 28 | group: grafana-agent 29 | 30 | - name: ensure unzip is installed 31 | apt: 32 | name: unzip 33 | state: present 34 | 35 | - name: Create directory for config 36 | file: 37 | path: "{{ agent_config_location }}" 38 | state: directory 39 | mode: '0755' 40 | 41 | 42 | - name: Download Grafana Agent binary 43 | get_url: 44 | url: "https://github.com/grafana/agent/releases/download/{{ agent_version }}/agent-linux-{{ linux_architecture }}.zip" 45 | dest: "/tmp/agent-linux.zip" 46 | mode: '0644' 47 | 48 | 49 | - name: Unarchive Grafana Agent binary 50 | unarchive: 51 | src: "/tmp/agent-linux.zip" 52 | dest: "{{ agent_binary_location }}" 53 | remote_src: true 54 | mode: '0755' 55 | 56 | 57 | 58 | 59 | - name: Create config file for Grafana Agent 60 | template: 61 | src: grafana-agent.yml.j2 62 | dest: "{{ agent_config_location }}/grafana-agent.yml" 63 | notify: restart grafana-agent 64 | 65 | 66 | 67 | 68 | - name: Create service file for Grafana Agent 69 | template: 70 | src: grafana-agent.service.j2 71 | dest: "/etc/systemd/system/grafana-agent.service" 72 | 73 | - name: Start Grafana Agent service 74 | systemd: 75 | daemon_reload: true 76 | name: grafana-agent 77 | enabled: true 78 | state: restarted 79 | -------------------------------------------------------------------------------- /ansible/roles/grafana/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - set_fact: 5 | agent_version: v0.30.2 6 | linux_architecture: amd64 7 | agent_binary_location: /usr/local/bin 8 | agent_config_location: /usr/local/etc/grafana 9 | 10 | - include_tasks: ./install.yml 11 | - include_tasks: ./configure.yml -------------------------------------------------------------------------------- /ansible/roles/grafana/templates/grafana-agent.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Grafana Agent 3 | 4 | [Service] 5 | User=grafana-agent 6 | ExecStart={{ agent_binary_location }}/agent-linux-{{ linux_architecture }} --config.file={{ agent_config_location }}/grafana-agent.yml -server.http.address=127.0.0.1:9090 -server.grpc.address=127.0.0.1:9091 7 | Restart=always 8 | RestartSec=5 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | 13 | 14 | -------------------------------------------------------------------------------- /ansible/roles/grafana/templates/grafana-agent.yml.j2: -------------------------------------------------------------------------------- 1 | integrations: 2 | node_exporter: 3 | enabled: true 4 | enable_collectors: 5 | - systemd 6 | - filesystem 7 | relabel_configs: 8 | - replacement: {{ inventory_hostname }} 9 | target_label: instance 10 | prometheus_remote_write: 11 | - basic_auth: 12 | username: {{ lookup('env', 'GRAFANA_AGENT_USERNAME') }} 13 | password: {{ lookup('env', 'GRAFANA_AGENT_PASSWORD') }} 14 | url: https://prometheus-prod-10-prod-us-central-0.grafana.net/api/prom/push 15 | 16 | logs: 17 | positions_directory: /home/grafana-agent/positions 18 | configs: 19 | - name: futureporn-logs 20 | clients: 21 | - url: {{ lookup('env', 'GRAFANA_PROMTAIL_URL') }} 22 | scrape_configs: 23 | - job_name: systemd-journal 24 | journal: 25 | max_age: 24h 26 | labels: 27 | instance: {{ inventory_hostname }} 28 | job: systemd-journal 29 | relabel_configs: 30 | - source_labels: ['__journal__systemd_unit'] 31 | target_label: 'unit' 32 | - source_labels: ['__journal__boot_id'] 33 | target_label: 'boot_id' 34 | - source_labels: ['__journal__transport'] 35 | target_label: 'transport' 36 | 37 | metrics: 38 | configs: 39 | - name: integrations 40 | remote_write: 41 | - basic_auth: 42 | username: {{ lookup('env', 'GRAFANA_AGENT_USERNAME') }} 43 | password: {{ lookup('env', 'GRAFANA_AGENT_PASSWORD') }} 44 | url: https://prometheus-prod-10-prod-us-central-0.grafana.net/api/prom/push 45 | global: 46 | scrape_interval: 60s 47 | wal_directory: /home/grafana-agent 48 | 49 | -------------------------------------------------------------------------------- /ansible/roles/ipfs-old/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: init ipfs 4 | ansible.builtin.command: 5 | args: ipfs init 6 | 7 | - name: restart ipfs 8 | ansible.builtin.systemd: 9 | name: ipfs 10 | state: restarted 11 | daemon_reload: yes 12 | tags: 13 | - gc 14 | -------------------------------------------------------------------------------- /ansible/roles/ipfs-old/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: Create the ipfs group 5 | ansible.builtin.user: 6 | name: ipfs 7 | 8 | - name: Create the ipfs user 9 | ansible.builtin.user: 10 | name: ipfs 11 | groups: ipfs 12 | create_home: yes 13 | home: /home/ipfs 14 | 15 | 16 | 17 | 18 | - name: Install ipfs system service 19 | ansible.builtin.template: 20 | src: templates/ipfs.service.j2 21 | dest: /etc/systemd/system/ipfs.service 22 | owner: root 23 | group: root 24 | mode: '0755' 25 | notify: 26 | - restart ipfs 27 | 28 | - name: create ufw HTTP exception for IPFS gateway 29 | community.general.ufw: 30 | rule: allow 31 | port: 80 32 | proto: tcp 33 | 34 | 35 | - name: create ufw HTTPS exception for IPFS gateway 36 | community.general.ufw: 37 | rule: allow 38 | port: 443 39 | proto: tcp 40 | 41 | - name: create ufw exception for IPFS swarm 42 | community.general.ufw: 43 | rule: allow 44 | port: 4000 45 | proto: tcp 46 | 47 | 48 | 49 | 50 | - name: start ipfs 51 | ansible.builtin.systemd: 52 | name: ipfs 53 | state: started 54 | daemon_reload: yes 55 | 56 | 57 | - name: configure ipfs StorageMax 58 | ansible.builtin.lineinfile: 59 | dest: /home/ipfs/.ipfs/config 60 | regexp: "StorageMax" 61 | line: "\"StorageMax\": \"{{ ipfs_storage_max }}\"," 62 | state: present 63 | owner: ipfs 64 | group: ipfs 65 | notify: restart ipfs 66 | 67 | 68 | - name: configure ipfs GCPeriod 69 | ansible.builtin.lineinfile: 70 | dest: /home/ipfs/.ipfs/config 71 | regexp: "GCPeriod" 72 | line: "\"GCPeriod\": \"{{ ipfs_gc_period }}\"," 73 | state: present 74 | owner: ipfs 75 | group: ipfs 76 | notify: restart ipfs 77 | -------------------------------------------------------------------------------- /ansible/roles/ipfs-old/templates/ipfs.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=IPFS Daemon 3 | After=network.target 4 | 5 | [Service] 6 | User=ipfs 7 | Group=ipfs 8 | Environment=IPFS_PATH=/home/ipfs/.ipfs 9 | ExecStart=/usr/local/bin/ipfs daemon --init --migrate --enable-gc 10 | ## the following ExecStartPre is a workaround for missing disk usage limit functionality in go-ipfs 11 | ## see https://github.com/ipfs/go-ipfs/issues/3066 12 | ExecStartPre=rm -rf /home/ipfs/.ipfs/blocks/ && sync 13 | StandardOutput=journal 14 | Restart=on-failure 15 | KillSignal=SIGINT 16 | 17 | [Install] 18 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /ansible/roles/ipfs/README.md: -------------------------------------------------------------------------------- 1 | ## Gateway 2 | 3 | Potential perf improvements 4 | 5 | * [ ] configure Peering lists, enabling gateway to stay connected with other futureporn gateways and with the futureporn cluster -------------------------------------------------------------------------------- /ansible/roles/ipfs/files/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sbtp.xyz 6 | 7 | 8 |
 9 | 	                                                            
10 |        ,--.     ,--.                                        
11 |  ,---. |  |-. ,-'  '-. ,---.     ,--.  ,--.,--. ,--.,-----. 
12 | (  .-' | .-. ''-.  .-'| .-. |     \  `'  /  \  '  / `-.  /  
13 | .-'  `)| `-' |  |  |  | '-' '.--. /  /.  \   \   '   /  `-. 
14 | `----'  `---'   `--'  |  |-' '--''--'  '--'.-'  /   `-----' 
15 |                       `--'                 `---'            
16 | 	
17 | 18 |
19 |

sbtp.xyz IPFS Cluster

20 |

Operated by cj@futureporn.net (Twitter)

21 |
22 | 23 |
24 |

Looking for VTuber videos? CJ_Clippy YouTube Channel (SFW)

25 |

Looking for VTuber porn? Futureporn.net (NSFW)

26 |
27 | 28 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: reload caddy 5 | ansible.builtin.systemd: 6 | name: caddy 7 | state: reloaded 8 | daemon_reload: yes 9 | 10 | - name: disable caddy 11 | ansible.builtin.systemd: 12 | state: stopped 13 | enabled: false 14 | daemon_reload: true 15 | ignore_errors: true 16 | 17 | - name: disable nginx 18 | ansible.builtin.systemd: 19 | state: disabled 20 | enabled: false 21 | daemon_reload: true 22 | ignore_errors: true 23 | 24 | - name: init ipfs 25 | ansible.builtin.command: 26 | args: ipfs init 27 | 28 | - name: restart ipfs 29 | ansible.builtin.systemd: 30 | name: ipfs 31 | state: restarted 32 | daemon_reload: yes 33 | 34 | - name: restart nginx 35 | ansible.builtin.systemd: 36 | name: nginx 37 | state: restarted 38 | daemon_reload: yes 39 | 40 | - name: reload nginx 41 | ansible.builtin.systemd: 42 | name: nginx 43 | state: reloaded 44 | daemon_reload: yes -------------------------------------------------------------------------------- /ansible/roles/ipfs/patrons-only-auth-caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | http_port 8080 3 | https_port 8443 4 | debug 5 | 6 | order authenticate before respond 7 | order authorize before basicauth 8 | 9 | security { 10 | oauth identity provider generic { 11 | realm patreon 12 | driver generic 13 | client_id {env.PATREON_CLIENT_ID} 14 | client_secret {env.PATREON_CLIENT_SECRET} 15 | scopes identity.memberships 16 | base_auth_url https://www.patreon.com/oauth2/authorize 17 | metadata_url 18 | } 19 | 20 | authorization policy patrons_only_policy { 21 | set token sources cookie 22 | crypto key sign-verify {env.JWT_SHARED_KEY} 23 | 24 | # @todo IDK what the redirect is, but I want to understand it before proceeding. 25 | # Do I need it? don't I? 26 | #disable auth redirect 27 | 28 | set auth url /oauth2/patreon 29 | 30 | acl rule { 31 | allow admin 32 | match role authp/admin 33 | allow stop log info 34 | } 35 | acl rule { 36 | allow patrons 37 | match role authp/patron 38 | allow stop log info 39 | } 40 | acl rule { 41 | vod default deny 42 | match any 43 | deny stop log warn 44 | } 45 | } 46 | 47 | authentication portal auth_portal { 48 | crypto key sign-verify {env.JWT_SHARED_KEY} 49 | backend patreon {env.PATREON_CLIENT_ID} {env.PATREON_CLIENT_SECRET} 50 | metadata_url 51 | base_auth_url https://www.patreon.com/oauth2/authorize 52 | transform user { 53 | match realm patreon @todo this needs to check patreon data 54 | action add role authp/patron 55 | ui link "My Website" / icon "las la-film" 56 | } 57 | transform user { 58 | let the admin in 59 | match email chris@grimtech.net 60 | action add role authp/admin 61 | } 62 | } 63 | } 64 | 65 | 66 | auth.futureporn.net { 67 | 68 | route /auth* { 69 | authenticate with auth_portal 70 | } 71 | 72 | route /ipfs/* { 73 | authorize with patrons_only_policy 74 | reverse_proxy /ipfs/* localhost:8080/ipfs/* { 75 | header_upstream Host {http.reverse_proxy.upstream.hostport} 76 | } 77 | } 78 | 79 | route { 80 | authorize with patrons_only_policy 81 | respond "app is running and you seem to be authorized!" 82 | } 83 | 84 | } 85 | } -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/caddy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | # nvjacobo.caddy takes care of the following 3 tasks. make sure that runs first. 5 | 6 | - name: Create the caddy group 7 | ansible.builtin.group: 8 | name: caddy 9 | 10 | 11 | - name: Create the caddy user 12 | ansible.builtin.user: 13 | name: caddy 14 | groups: caddy 15 | create_home: yes 16 | home: /home/caddy 17 | ignore_errors: true 18 | 19 | 20 | - name: Install caddy system service 21 | ansible.builtin.template: 22 | src: templates/caddy.service.j2 23 | dest: /etc/systemd/system/caddy.service 24 | owner: root 25 | group: root 26 | mode: '0755' 27 | notify: 28 | - reload caddy 29 | 30 | 31 | - name: Create /usr/local/bin/etc/caddy 32 | ansible.builtin.file: 33 | path: /usr/local/bin/etc/caddy 34 | state: directory 35 | owner: caddy 36 | group: caddy 37 | mode: '0600' 38 | notify: reload caddy 39 | 40 | 41 | - name: copy custom-built caddy to server 42 | ansible.builtin.copy: 43 | remote_src: false 44 | src: /home/chris/tmpdev/caddy-security/bin/authp 45 | dest: /usr/local/bin/caddy 46 | mode: '0755' 47 | 48 | 49 | - name: Configure Caddy 50 | ansible.builtin.template: 51 | src: templates/Caddyfile.j2 52 | dest: /usr/local/etc/caddy/Caddyfile 53 | owner: root 54 | group: root 55 | mode: '0755' 56 | notify: reload caddy 57 | 58 | 59 | - name: stop nginx 60 | ansible.builtin.systemd: 61 | name: nginx 62 | state: stopped 63 | notify: 64 | - disable nginx 65 | ignore_errors: true 66 | 67 | 68 | - name: start & enable caddy 69 | ansible.builtin.systemd: 70 | name: caddy 71 | state: started 72 | enabled: true 73 | daemon_reload: true 74 | ignore_errors: true 75 | 76 | 77 | # - name: mkdir for caddy 78 | # ansible.builtin.file: 79 | # state: directory 80 | # dest: /usr/local/etc/caddy 81 | # owner: root 82 | # group: root 83 | # mode: '0755' 84 | 85 | # - name: mkdir for www 86 | # file: 87 | # state: directory 88 | # dest: /var/www/sbtp.xyz 89 | # owner: caddy 90 | # group: caddy 91 | # mode: '0755' 92 | 93 | # - name: copy index.html to server 94 | # ansible.builtin.copy: 95 | # remote_src: false 96 | # src: files/index.html 97 | # dest: /var/www/sbtp.xyz/index.html 98 | # owner: caddy 99 | # group: caddy 100 | # mode: '0744' 101 | 102 | 103 | 104 | # - name: Install caddy init file 105 | # ansible.builtin.template: 106 | # src: templates/caddy.service.j2 107 | # dest: -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/certbot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install certbot-dns-luadns 4 | ansible.builtin.pip: 5 | name: certbot-dns-luadns 6 | 7 | # shell: "cd /opt/certbot/certbot-dns-luadns && python setup.py install" 8 | 9 | - name: Create certbot settings folder 10 | file: 11 | path: /etc/letsencrypt 12 | state: directory 13 | owner: root 14 | group: root 15 | mode: '0700' 16 | 17 | - name: Create Certbot ini files 18 | template: 19 | src: "{{ item.src }}" 20 | dest: "{{ item.dest }}" 21 | owner: root 22 | group: root 23 | mode: '0600' 24 | with_items: 25 | - { src: 'certbot_luadns.ini.j2', dest: '/etc/letsencrypt/luadns.ini' } 26 | - { src: 'letsencrypt_cli.ini.j2', dest: '/etc/letsencrypt/cli.ini' } 27 | 28 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # # see https://github.com/insanity54/futureporn/issues/73 4 | # - name: set gateway DNS 5 | # vultr.cloud.dns_record: 6 | # api_key: "{{ lookup('env', 'VULTR_API_KEY')}}" 7 | # record_type: A 8 | # name: ipfs 9 | # domain: sbtp.xyz 10 | # data: "{{ ansible_default_ipv4.address }}" 11 | # ttl: 3600 12 | # multiple: yes 13 | # state: present 14 | # delegate_to: localhost 15 | # run_once: yes -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: Allow everything and enable UFW 5 | community.general.ufw: 6 | state: enabled 7 | policy: allow 8 | 9 | 10 | - name: Set logging 11 | community.general.ufw: 12 | logging: 'on' 13 | 14 | 15 | # ufw supports connection rate limiting, which is useful for protecting 16 | # against brute-force login attacks. ufw will deny connections if an IP 17 | # address has attempted to initiate 6 or more connections in the last 18 | # 30 seconds. See http://www.debian-administration.org/articles/187 19 | # for details. Typical usage is: 20 | - community.general.ufw: 21 | rule: limit 22 | port: ssh 23 | proto: tcp 24 | 25 | - community.general.ufw: 26 | rule: limit 27 | port: ssh 28 | proto: tcp 29 | 30 | 31 | 32 | 33 | - name: Disallow direct IPFS daemon access 34 | community.general.ufw: 35 | rule: reject 36 | port: '8080' 37 | log: true 38 | 39 | - name: Allow IPFS gateway access for 127.0.0.1 40 | ufw: 41 | rule: allow 42 | port: '8080' 43 | proto: tcp 44 | src: 127.0.0.1 45 | 46 | # - name: Disallow direct metrics access 47 | # community.general.ufw: 48 | # rule: reject 49 | # port: '8888' 50 | # log: true 51 | 52 | - name: Allow IPFS api access only on localhost 53 | ufw: 54 | rule: allow 55 | port: '5001' 56 | proto: tcp 57 | src: 127.0.0.1 58 | 59 | 60 | - name: Allow HTTP 61 | ufw: 62 | rule: allow 63 | port: '80' 64 | proto: tcp 65 | 66 | 67 | - name: Allow HTTPS 68 | ufw: 69 | rule: allow 70 | port: '443' 71 | proto: tcp 72 | 73 | 74 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/ipfs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create the ipfs group 4 | ansible.builtin.group: 5 | name: ipfs 6 | 7 | - name: Create the ipfs user 8 | ansible.builtin.user: 9 | name: ipfs 10 | group: ipfs 11 | create_home: false 12 | home: /home/ipfs 13 | 14 | - name: Create ipfs home directory 15 | ansible.builtin.file: 16 | state: directory 17 | path: /home/ipfs 18 | owner: ipfs 19 | group: ipfs 20 | mode: '0750' 21 | 22 | - name: Download kubo 23 | ansible.builtin.get_url: 24 | url: "https://dist.ipfs.tech/kubo/{{ ipfs_kubo_version }}/kubo_{{ ipfs_kubo_version }}_linux-amd64.tar.gz" 25 | dest: /root/ 26 | checksum: "{{ ipfs_kubo_checksum }}" 27 | 28 | - name: unarchive kubo 29 | ansible.builtin.unarchive: 30 | remote_src: yes 31 | src: "/root/kubo_{{ ipfs_kubo_version }}_linux-amd64.tar.gz" 32 | dest: /root/ 33 | 34 | - name: install kubo 35 | ansible.builtin.copy: 36 | remote_src: yes 37 | src: /root/kubo/ipfs 38 | dest: /usr/local/bin/ipfs 39 | mode: '0755' 40 | owner: root 41 | group: root 42 | 43 | - name: Install ipfs system service 44 | ansible.builtin.template: 45 | src: templates/ipfs.service.j2 46 | dest: /etc/systemd/system/ipfs.service 47 | owner: root 48 | group: root 49 | mode: '0755' 50 | notify: 51 | - restart ipfs 52 | 53 | - name: create ufw exception for IPFS swarm 54 | community.general.ufw: 55 | rule: allow 56 | port: 4000 57 | proto: tcp 58 | 59 | 60 | - name: init IPFS 61 | become: yes 62 | become_user: ipfs 63 | command: /usr/local/bin/ipfs init --empty-repo 64 | args: 65 | creates: "/home/ipfs/.ipfs/config" 66 | notify: restart ipfs 67 | 68 | - name: install ipfs config 69 | template: 70 | src: templates/ipfs-config.json.j2 71 | dest: /home/ipfs/.ipfs/config 72 | owner: ipfs 73 | group: ipfs 74 | mode: '0600' 75 | notify: 76 | - restart ipfs 77 | 78 | - name: start ipfs 79 | ansible.builtin.systemd: 80 | name: ipfs 81 | state: started 82 | daemon_reload: yes 83 | enabled: yes 84 | 85 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - include_tasks: sysctl.yml 5 | - include_tasks: ipfs.yml 6 | - include_tasks: caddy.yml 7 | - include_tasks: firewall.yml -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - ansible.builtin.systemd: 4 | name: caddy 5 | state: stopped 6 | notify: 7 | - disable caddy 8 | 9 | - name: install nginx 10 | apt: 11 | name: nginx 12 | state: present 13 | 14 | - name: delete default nginx config 15 | ansible.builtin.file: 16 | dest: /etc/nginx/sites-enabled/default 17 | state: absent 18 | notify: 19 | - reload nginx 20 | 21 | - name: Configure Nginx 22 | ansible.builtin.template: 23 | src: templates/nginx.j2 24 | dest: /etc/nginx/sites-available/sbtp.xyz 25 | owner: root 26 | group: root 27 | mode: '0755' 28 | notify: restart nginx 29 | 30 | - name: Create symlink to sites-available 31 | file: 32 | dest: /etc/nginx/sites-enabled/sbtp.xyz 33 | src: /etc/nginx/sites-available/sbtp.xyz 34 | owner: root 35 | group: root 36 | state: link 37 | 38 | 39 | # /var/www/html/ -------------------------------------------------------------------------------- /ansible/roles/ipfs/tasks/sysctl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size 4 | - name: increase UDP Receive Buffer Size 5 | ansible.posix.sysctl: 6 | name: net.core.rmem_max 7 | value: 2500000 8 | sysctl_set: yes -------------------------------------------------------------------------------- /ansible/roles/ipfs/templates/Caddyfile.j2: -------------------------------------------------------------------------------- 1 | { 2 | email "cj@futureporn.net" 3 | debug 4 | http_port 80 5 | https_port 443 6 | 7 | order authenticate before respond 8 | order authorize before basicauth 9 | 10 | security { 11 | oauth identity provider patreon { 12 | realm patreon 13 | driver patreon 14 | client_id {env.PATREON_CLIENT_ID} 15 | client_secret {env.PATREON_CLIENT_SECRET} 16 | scopes identity.memberships 17 | } 18 | 19 | authorization policy patrons_only_policy { 20 | set auth url /auth/oauth2/patreon 21 | crypto key verify {env.JWT_SHARED_KEY} 22 | allow roles authp/admin authp/patron 23 | validate bearer header 24 | inject headers with claims 25 | } 26 | 27 | authentication portal auth_portal { 28 | cookie domain futureporn.net 29 | cookie lifetime 2629746 # 1 month 30 | crypto key sign-verify {env.JWT_SHARED_KEY} 31 | 32 | transform user { 33 | match realm patreon 34 | match role patreon.com/campaign/8012692 35 | action add role authp/patron 36 | ui link "Futureporn IPFS Gateway" /ipfs icon "las la-film" 37 | } 38 | 39 | 40 | transform user { 41 | match realm patreon 42 | match sub patreon.com/user/20828619 43 | action add role authp/admin 44 | ui link "IPFS Gateway (hi, admin!)" /ipfs icon "las la-film" 45 | } 46 | } 47 | } 48 | } 49 | 50 | 51 | 52 | 53 | gw.futureporn.net { 54 | route /auth* { 55 | authenticate with auth_portal 56 | } 57 | 58 | route /ipfs/* { 59 | authorize with patrons_only_policy 60 | reverse_proxy /ipfs/* { 61 | header_up +X-Forwarded-Proto https 62 | to http://127.0.0.1:8080 63 | } 64 | } 65 | 66 | route /taco { 67 | authorize with patrons_only_policy 68 | respond "Tacos are great!" 69 | } 70 | 71 | route / { 72 | respond "Patron-only IPFS gateway for futureporn.net" 73 | } 74 | 75 | route { 76 | authorize with patrons_only_policy 77 | respond "app is running and you seem to be authorized!" 78 | } 79 | 80 | handle_errors { 81 | respond "{err.status_code} {err.status_text}" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/templates/caddy.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Caddy 3 | Documentation=https://caddyserver.com/docs/ 4 | After=network.target network-online.target 5 | Requires=network-online.target 6 | 7 | [Service] 8 | Type=notify 9 | User=caddy 10 | Group=caddy 11 | ExecStart=/usr/local/bin/caddy run --environ --config /usr/local/etc/caddy/Caddyfile 12 | ExecReload=/usr/local/bin/caddy reload --config /usr/local/etc/caddy/Caddyfile --force 13 | TimeoutStopSec=5s 14 | LimitNOFILE=1048576 15 | LimitNPROC=512 16 | PrivateTmp=true 17 | ProtectSystem=full 18 | AmbientCapabilities=CAP_NET_BIND_SERVICE 19 | Environment=PATREON_CLIENT_ID={{ lookup('env', 'PATREON_CLIENT_ID') }} 20 | Environment=PATREON_CLIENT_SECRET={{ lookup('env', 'PATREON_CLIENT_SECRET') }} 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/templates/certbot_luadns.ini.j2: -------------------------------------------------------------------------------- 1 | # LuaDNS API credentials used by Certbot 2 | dns_luadns_email = "{{ lookup('env', 'LUA_DNS_EMAIL') }}" 3 | dns_luadns_token = "{{ lookup('env', 'LUA_DNS_TOKEN') }}" -------------------------------------------------------------------------------- /ansible/roles/ipfs/templates/ipfs.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=IPFS Daemon 3 | After=network.target 4 | 5 | [Service] 6 | User=ipfs 7 | Group=ipfs 8 | Environment=IPFS_PATH=/home/ipfs/.ipfs 9 | ExecStart=/usr/local/bin/ipfs daemon --init --migrate 10 | StandardOutput=journal 11 | Restart=always 12 | KillSignal=SIGINT 13 | RestartSec=8 14 | 15 | [Install] 16 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /ansible/roles/ipfs/templates/letsencrypt_cli.ini.j2: -------------------------------------------------------------------------------- 1 | # Let's Encrypt site-wide configuration 2 | dns-luadns-credentials = /etc/letsencrypt/luadns.ini 3 | 4 | # Use the ACME v2 staging URI for testing things 5 | server = https://acme-staging-v02.api.letsencrypt.org/directory 6 | 7 | # Production ACME v2 API endpoint 8 | #server = https://acme-v02.api.letsencrypt.org/directory 9 | 10 | -------------------------------------------------------------------------------- /ansible/roles/ipfs/templates/nginx.j2: -------------------------------------------------------------------------------- 1 | server { 2 | server_name clussy.futureporn.net; 3 | 4 | location /ipfs { 5 | proxy_pass http://127.0.0.1:8080; 6 | proxy_set_header Host clussy.futureporn.net; 7 | } 8 | 9 | location /ipns { 10 | proxy_pass http://127.0.0.1:8080; 11 | proxy_set_header Host clussy.futureporn.net; 12 | } 13 | } 14 | 15 | server { 16 | server_name *.clussy.futureporn.net; 17 | 18 | location / { 19 | proxy_pass http://127.0.0.1:8080; 20 | proxy_set_header Host $host; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /ansible/roles/manager/README.md: -------------------------------------------------------------------------------- 1 | ## futureporn-manager 2 | 3 | futureporn-manager is DEPRECATED 4 | 5 | Previously, futureporn-manager was used to workaround a 16GB RAM requirement for ipfs.storage uploads. We are no longer using ipfs.storage, so we can use a smaller RAM size VPS. 6 | 7 | futureporn-capture is the replacement for futureporn-manager. -------------------------------------------------------------------------------- /ansible/roles/manager/templates/sshConfig.j2: -------------------------------------------------------------------------------- 1 | Host github.com 2 | User git 3 | Port 22 4 | Hostname github.com 5 | IdentityFile ~/.ssh/futureporn 6 | TCPKeepAlive yes 7 | IdentitiesOnly yes 8 | StrictHostKeyChecking no 9 | -------------------------------------------------------------------------------- /ansible/roles/prometheus/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: ensure node_exporter is installed 4 | apt: 5 | name: prometheus-node-exporter 6 | state: present 7 | 8 | - name: ensure node_exporter is running 9 | systemd: 10 | name: prometheus-node-exporter 11 | state: started 12 | 13 | - name: open firewall for prometheus metrics 14 | ufw: 15 | port: 9100 16 | proto: tcp 17 | rule: allow 18 | -------------------------------------------------------------------------------- /ansible/roles/qa/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart qa 4 | ansible.builtin.systemd: 5 | name: qa 6 | state: restarted 7 | -------------------------------------------------------------------------------- /ansible/roles/qa/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | - name: Ensure futureporn is updated 5 | git: 6 | repo: https://github.com/insanity54/futureporn 7 | dest: /root/futureporn 8 | depth: 1 9 | update: yes 10 | force: yes 11 | notify: 12 | - restart qa 13 | 14 | - name: ensure yarn is installed 15 | community.general.npm: 16 | name: yarn 17 | global: yes 18 | 19 | - name: install futureporn/qa 20 | community.general.yarn: 21 | path: /root/futureporn/qa 22 | notify: 23 | - restart qa 24 | 25 | - name: Install qa system service 26 | ansible.builtin.template: 27 | src: templates/qa.service.j2 28 | dest: /etc/systemd/system/qa.service 29 | owner: root 30 | group: root 31 | mode: '0755' 32 | notify: 33 | - restart qa 34 | 35 | - name: Enable qa system service 36 | systemd: 37 | name: qa 38 | state: started 39 | daemon_reload: yes 40 | enabled: yes 41 | notify: 42 | - restart qa 43 | 44 | # - name: Close firewall so only acessible via reverse_proxy 45 | # ufw: 46 | # 5345 47 | # # @todo 48 | 49 | -------------------------------------------------------------------------------- /ansible/roles/qa/templates/qa.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description=Futureporn Quality Assurance 6 | After=network.target 7 | 8 | [Service] 9 | ExecStart=/usr/bin/env node /root/futureporn/qa/index.js 10 | WorkingDirectory=/root/futureporn/qa 11 | Restart=always 12 | RestartSec=5 13 | Environment=PATH=/usr/bin:/usr/local/bin 14 | Environment=NODE_ENV=production 15 | Environment=DEBUG="futureporn/*" 16 | Environment=IPFS_CLUSTER_HTTP_API_USERNAME={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_USERNAME') }} 17 | Environment=IPFS_CLUSTER_HTTP_API_PASSWORD={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_PASSWORD') }} 18 | Environment=IPFS_CLUSTER_HTTP_API_MULTIADDR={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_MULTIADDR') }} 19 | Environment=QA_USERNAME={{ lookup('env', 'QA_USERNAME') }} 20 | Environment=QA_PASSWORD={{ lookup('env', 'QA_PASSWORD') }} 21 | Environment=PORT=5345 22 | Type=simple 23 | -------------------------------------------------------------------------------- /ansible/roles/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | roles: 4 | - geerlingguy.nodejs 5 | - geerlingguy.certbot 6 | - nvjacobo.caddy 7 | - jeffbr13.ipfs 8 | - name: madoke.ansible-ipfs-cluster 9 | src: https://github.com/madoke/ansible-ipfs-cluster.git 10 | type: git 11 | version: 3bd23433fdae86f342eeb23a5857119a87adbac9 12 | 13 | collections: 14 | - ngine_io.vultr 15 | - vultr.cloud 16 | - grafana.grafana 17 | # In the mean time, I'm using my vultr.cloud fork 18 | # - name: https://github.com/insanity54/ansible-collection-vultr.git 19 | # type: git 20 | # version: 70a16ab4a424753713c66c133246e285eb7a1804 21 | -------------------------------------------------------------------------------- /ansible/roles/scout/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart scout 4 | ansible.builtin.systemd: 5 | name: scout 6 | state: restarted 7 | -------------------------------------------------------------------------------- /ansible/roles/scout/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | 5 | 6 | 7 | - name: Ensure futureporn is updated 8 | git: 9 | repo: https://github.com/insanity54/futureporn 10 | dest: /home/futureporn/futureporn 11 | depth: 1 12 | update: yes 13 | force: yes 14 | become: true 15 | become_user: futureporn 16 | 17 | - name: ensure yarn is installed 18 | community.general.npm: 19 | name: yarn 20 | global: yes 21 | become: true 22 | become_user: futureporn 23 | 24 | # no longer needed because https://yarnpkg.com/features/zero-installs 25 | # - name: install scout 26 | # community.general.yarn: 27 | # path: /home/futureporn/futureporn/packages/scout 28 | # become: true 29 | # become_user: futureporn 30 | 31 | - name: create datadir 32 | file: 33 | path: /home/futureporn/.local/share/futureporn/scout 34 | state: directory 35 | owner: futureporn 36 | group: futureporn 37 | 38 | - name: Install scout system service 39 | ansible.builtin.template: 40 | src: templates/scout.service.j2 41 | dest: /etc/systemd/system/scout.service 42 | owner: root 43 | group: root 44 | mode: '0755' 45 | notify: 46 | - restart scout 47 | 48 | - name: Enable scout system service 49 | systemd: 50 | name: scout 51 | state: started 52 | daemon_reload: yes 53 | enabled: yes 54 | notify: 55 | - restart scout 56 | 57 | -------------------------------------------------------------------------------- /ansible/roles/scout/templates/scout.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description=Futureporn Scout 6 | After=network.target 7 | 8 | [Service] 9 | ExecStart=/usr/bin/env yarn node index 10 | WorkingDirectory=/home/futureporn/futureporn/packages/scout 11 | Restart=always 12 | RestartSec=5 13 | User=futureporn 14 | Group=futureporn 15 | Environment=PATH=/usr/bin:/usr/local/bin 16 | Environment=NODE_ENV=production 17 | Environment=DEBUG="futureporn/*" 18 | Environment=TWITTER_API_KEY={{ lookup('env', 'TWITTER_API_KEY') }} 19 | Environment=TWITTER_API_KEY_SECRET={{ lookup('env', 'TWITTER_API_KEY_SECRET') }} 20 | Environment=TWITTER_ACCESS_TOKEN={{ lookup('env', 'TWITTER_ACCESS_TOKEN') }} 21 | Environment=TWITTER_ACCESS_TOKEN_SECRET={{ lookup('env', 'TWITTER_ACCESS_TOKEN_SECRET') }} 22 | Environment=IPFS_CLUSTER_HTTP_API_USERNAME={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_USERNAME') }} 23 | Environment=IPFS_CLUSTER_HTTP_API_PASSWORD={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_PASSWORD') }} 24 | Environment=IPFS_CLUSTER_HTTP_API_MULTIADDR={{ lookup('env', 'IPFS_CLUSTER_HTTP_API_MULTIADDR') }} 25 | Environment=POSTGRES_HOST={{ lookup('env', 'POSTGRES_HOST') }} 26 | Environment=POSTGRES_USERNAME={{ lookup('env', 'POSTGRES_USERNAME') }} 27 | Environment=POSTGRES_PASSWORD={{ lookup('env', 'POSTGRES_PASSWORD') }} 28 | Environment=FUTUREPORN_WORKDIR={{ lookup('env', 'FUTUREPORN_WORKDIR' )}} 29 | 30 | Type=simple 31 | -------------------------------------------------------------------------------- /ansible/roles/spinup/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: refresh inventory 4 | meta: refresh_inventory 5 | ignore_errors: true # https://github.com/ansible/ansible/issues/50306 6 | # https://github.com/ansible/ansible/pull/50608 -------------------------------------------------------------------------------- /ansible/roles/spinup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: var=instance_count 4 | 5 | - name: Create a Vultr instance 6 | vultr.cloud.instance: 7 | label: "futureporn-{{ instance_type }}{{ item }}" 8 | hostname: "futureporn-{{ instance_type }}{{ item }}" 9 | # package requirements differ slightly for 10 | # cluster vs capture roles so 11 | # we only want to set packages that are commonly 12 | # used for all roles 13 | user_data: | 14 | #cloud-config 15 | packages: 16 | - python3 17 | - python3-pip 18 | - mg 19 | - git 20 | - magic-wormhole 21 | - mosh 22 | plan: "{{ instance_plan }}" 23 | ddos_protection: false 24 | backups: false 25 | enable_ipv6: true 26 | ssh_keys: 27 | - LEO-COMPUTE 28 | tags: 29 | - "{{ instance_type }}" 30 | region: dfw 31 | os: Ubuntu 22.04 LTS x64 32 | loop: "{{ range(1, (instance_count|int+1), 1)|list }}" 33 | 34 | - meta: refresh_inventory -------------------------------------------------------------------------------- /ansible/roles/spinup/tasks/terraform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - community.general.terraform: 4 | project_path: ./terraform 5 | state: present 6 | variables: 7 | instance_count: "{{ instance_count }}" 8 | vultr_ssh_keys: "{{ vultr_ssh_keys | list | to_json }}" 9 | 10 | - meta: refresh_inventory 11 | 12 | 13 | -------------------------------------------------------------------------------- /ansible/roles/uppy/files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "companion", 4 | "version": "0.0.1", 5 | "main": "companion.js", 6 | "license": "Unlicense", 7 | "private": true, 8 | "dependencies": { 9 | "@bugsnag/cuid": "^3.0.0", 10 | "@uppy/companion": "^3.5.2", 11 | "body-parser": "^1.20.0", 12 | "connect-redis": "^6.1.3", 13 | "dotenv": "^16.0.1", 14 | "emailjs": "^4.0.0", 15 | "express": "^4.18.1", 16 | "express-session": "^1.17.3", 17 | "ioredis": "^5.0.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ansible/roles/uppy/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: reload caddy 4 | ansible.builtin.systemd: 5 | name: caddy 6 | state: reloaded 7 | daemon_reload: yes 8 | 9 | - name: restart companion 10 | ansible.builtin.systemd: 11 | name: companion 12 | state: restarted 13 | daemon_reload: yes 14 | 15 | -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/caddy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create /var/www 4 | ansible.builtin.file: 5 | path: /var/www 6 | state: directory 7 | owner: caddy 8 | group: caddy 9 | mode: '0600' 10 | notify: reload caddy 11 | 12 | 13 | - name: Configure Caddy 14 | ansible.builtin.template: 15 | src: templates/Caddyfile.j2 16 | dest: /etc/caddy/Caddyfile 17 | owner: caddy 18 | group: caddy 19 | mode: '0600' 20 | notify: reload caddy -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/companion.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | 5 | - name: ensure yarn is installed 6 | community.general.npm: 7 | name: yarn 8 | global: yes 9 | become: yes 10 | become_user: uppy 11 | 12 | - name: Create data directory for uppy companion 13 | file: 14 | path: /home/uppy/companion 15 | state: directory 16 | owner: uppy 17 | group: uppy 18 | mode: '0755' 19 | 20 | - name: Create data directory for uppy companion data 21 | file: 22 | path: /home/uppy/data 23 | state: directory 24 | owner: uppy 25 | group: uppy 26 | mode: '0755' 27 | 28 | - name: create companion config file 29 | ansible.builtin.template: 30 | src: templates/config.js.j2 31 | dest: /home/uppy/companion/config.js 32 | owner: uppy 33 | group: uppy 34 | notify: 35 | - restart companion 36 | 37 | - name: Install companion app 38 | ansible.builtin.copy: 39 | src: "{{ item }}" 40 | dest: "/home/uppy/companion/{{ item }}" 41 | owner: uppy 42 | group: uppy 43 | mode: '0755' 44 | notify: 45 | - restart companion 46 | loop: 47 | - companion.js 48 | - package.json 49 | - yarn.lock 50 | 51 | - name: Install node packages 52 | community.general.yarn: 53 | path: /home/uppy/companion 54 | become: yes 55 | become_user: uppy 56 | notify: 57 | - restart companion 58 | 59 | 60 | - name: Install companion system service 61 | ansible.builtin.template: 62 | src: templates/companion.service.j2 63 | dest: /etc/systemd/system/companion.service 64 | owner: root 65 | group: root 66 | mode: '0755' 67 | notify: 68 | - restart companion 69 | 70 | - name: Enable companion system service 71 | systemd: 72 | name: companion 73 | daemon_reload: yes 74 | enabled: yes 75 | notify: 76 | - restart companion 77 | 78 | -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/dns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: ensure we have facts 4 | ansible.builtin.setup: 5 | when: not ansible_default_ipv4 6 | 7 | - name: prompt user to set DNS manually 8 | debug: 9 | msg: "Namecheap has no API. Please set redis.futureporn.net and uppy.futureporn.net A records at https://ap.www.namecheap.com/Domains/DomainControlPanel/futureporn.net/advancedns. IP address is {{ ansible_default_ipv4.address }}" 10 | -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # i dont think we need this. Instead, we put the env vars in system service 4 | # - name: Set env vars 5 | # ansible.builtin.lineinfile: 6 | # dest: /root/.bashrc 7 | # line: "export {{ item }}={{ lookup('env', item ) }}" 8 | # state: present 9 | # loop: 10 | # - COMPANION_GOOGLE_KEY 11 | # - COMPANION_GOOGLE_SECRET 12 | # - COMPANION_SECRET 13 | # - COMPANION_DOMAIN 14 | # - COMPANION_DATADIR 15 | # - COMPANION_PROTOCOL 16 | # - COMPANION_PORT 17 | # - COMPANION_PATH 18 | # - COMPANION_HIDE_WELCOME 19 | # - COMPANION_HIDE_METRICS 20 | # - COMPANION_IMPLICIT_PATH 21 | # - COMPANION_CLIENT_ORIGINS 22 | # - COMPANION_REDIS_URL 23 | # - COMPANION_DROPBOX_KEY 24 | # - COMPANION_DROPBOX_SECRET 25 | # - COMPANION_DROPBOX_SECRET_FILE -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: create ufw exception for http 4 | community.general.ufw: 5 | rule: allow 6 | port: '80' 7 | proto: tcp 8 | 9 | - name: create ufw exception for https 10 | community.general.ufw: 11 | rule: allow 12 | port: '443' 13 | proto: tcp -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: screen.yml 4 | - include_tasks: firewall.yml 5 | - include_tasks: user.yml 6 | - include_tasks: redis.yml 7 | - include_tasks: dns.yml 8 | - include_tasks: caddy.yml 9 | - include_tasks: companion.yml -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/redis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install redis 4 | become: yes 5 | apt: 6 | pkg: 7 | - redis-server 8 | state: present 9 | update_cache: yes 10 | -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/screen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Download screen configuration 4 | git: 5 | repo: 'https://github.com/insanity54/dotfiles' 6 | dest: /root/dotfiles 7 | 8 | - name: Configure screen 9 | copy: 10 | src: /root/dotfiles/.screenrc 11 | dest: /root/.screenrc 12 | remote_src: yes -------------------------------------------------------------------------------- /ansible/roles/uppy/tasks/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | 5 | - name: Create the caddy group 6 | ansible.builtin.user: 7 | name: caddy 8 | 9 | - name: Create the caddy user 10 | ansible.builtin.user: 11 | name: caddy 12 | groups: caddy 13 | create_home: no 14 | 15 | - name: Create the uppy group 16 | ansible.builtin.user: 17 | name: uppy 18 | 19 | - name: Create the uppy user 20 | ansible.builtin.user: 21 | name: uppy 22 | groups: uppy 23 | create_home: yes 24 | home: /home/uppy -------------------------------------------------------------------------------- /ansible/roles/uppy/templates/Caddyfile.j2: -------------------------------------------------------------------------------- 1 | { 2 | email "cj@futureporn.net" 3 | } 4 | 5 | uppy.futureporn.net { 6 | reverse_proxy 127.0.0.1:3000 7 | } 8 | 9 | redis.futureporn.net { 10 | reverse_proxy 127.0.0.1:6379 11 | } 12 | -------------------------------------------------------------------------------- /ansible/roles/uppy/templates/companion.service.j2: -------------------------------------------------------------------------------- 1 | [Install] 2 | WantedBy=multi-user.target 3 | 4 | [Unit] 5 | Description=Uppy Companion for Futureporn 6 | After=network.target 7 | StartLimitIntervalSec=0 8 | 9 | [Service] 10 | User=uppy 11 | Group=uppy 12 | ExecStart=/home/uppy/companion/companion.js 13 | WorkingDirectory=/home/uppy/companion 14 | Restart=always 15 | RestartSec=3 16 | Environment=PATH=/usr/bin:/usr/local/bin 17 | Environment=NODE_ENV=production 18 | Environment=DEBUG=uppy 19 | Environment=SMTP_USER={{ lookup('env', 'SMTP_USER') }} 20 | Environment=SMTP_PASSWORD={{ lookup('env', 'SMTP_PASSWORD') }} 21 | Environment=SMTP_HOST={{ lookup('env', 'SMTP_HOST') }} 22 | Environment=SESSION_SECRET={{ lookup('env', 'SESSION_SECRET') }} 23 | Type=simple 24 | -------------------------------------------------------------------------------- /ansible/roles/uppy/templates/config.js.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | const config = { 4 | debug: true, 5 | metrics: true, 6 | server: { 7 | host: "{{ lookup('env', 'COMPANION_DOMAIN') }}", 8 | protocol: "{{ lookup('env', 'COMPANION_PROTOCOL') }}", 9 | port: "{{ lookup('env', 'COMPANION_PORT') }}" 10 | }, 11 | streamingUpload: true, 12 | corsOrigins: "https://futureporn.net", 13 | redisUrl: "{{ lookup('env', 'COMPANION_REDIS_URL') }}", 14 | secret: "{{ lookup('env', 'COMPANION_SECRET') }}", 15 | filePath: "{{ lookup('env', 'COMPANION_DATADIR') }}", 16 | uploadUrls: ["https://uppy.futureporn.net/"], 17 | providerOptions: { 18 | s3: { 19 | "key": "{{ lookup('env', 'COMPANION_AWS_KEY') }}", 20 | "secret": "{{ lookup('env', 'COMPANION_AWS_SECRET') }}", 21 | "bucket": "{{ lookup('env', 'COMPANION_AWS_BUCKET') }}", 22 | "endpoint": "{{ lookup('env', 'COMPANION_AWS_ENDPOINT') }}" 23 | }, 24 | drive: { 25 | key: "{{ lookup('env', 'COMPANION_GOOGLE_KEY') }}", 26 | secret: "{{ lookup('env', 'COMPANION_GOOGLE_SECRET') }}", 27 | }, 28 | dropbox: { 29 | key: "{{ lookup('env', 'COMPANION_DROPBOX_KEY') }}", 30 | secret: "{{ lookup('env', 'COMPANION_DROPBOX_SECRET') }}" 31 | }, 32 | } 33 | } 34 | 35 | export default config; -------------------------------------------------------------------------------- /ansible/uppy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | ## spinup is kinda broken. 5 | ## the BEST WAY to move forward on this is to FIX THE VULTR ANSIBLE MODULE 6 | ## 7 | ## https://github.com/ngine-io/ansible-collection-vultr/issues/17 8 | ## It's broken because it uses Vultr API v1. I think Vultr API v2 is needed to spin up newer instance types 9 | 10 | - hosts: localhost 11 | gather_facts: no 12 | vars: 13 | - vps_hostname: "futureporn-uppy" 14 | - vps_plan: vc2-1c-1gb 15 | - vps_region: dfw 16 | roles: 17 | - role: spinup 18 | 19 | 20 | 21 | - hosts: futureporn-uppy 22 | # gather_facts: no 23 | vars: 24 | - ansible_user: root 25 | roles: 26 | # - role: base 27 | # - role: nvjacobo.caddy 28 | # - role: geerlingguy.nodejs 29 | - role: uppy 30 | environment: 31 | PATH: "{{ ansible_env.PATH }}:/home/uppy/.npm/bin/" 32 | NPM_CONFIG_PREFIX: /home/uppy/.npm -------------------------------------------------------------------------------- /ansible/vultr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: vultr 3 | -------------------------------------------------------------------------------- /misc/futureporn-logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/misc/futureporn-logo.xcf -------------------------------------------------------------------------------- /misc/futureporn-logo_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/misc/futureporn-logo_256.png -------------------------------------------------------------------------------- /misc/futureporn-logo_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/misc/futureporn-logo_64.png -------------------------------------------------------------------------------- /misc/patreon-streams.md: -------------------------------------------------------------------------------- 1 | # Patreon Streams 2 | 3 | I occasionally come across VODs from what I believe are Patreon streams, streams which Mel did not mention via Twitter. As these streams are private, supporter-only events, I don't want to post these streams on Futureporn. I'm referencing the dates here for historical purposes. 4 | 5 | 6 | * 2022-01-27 7 | * 2021-01-23 8 | * 2020-07-24 9 | * 2020-05-21 10 | * 2020-04-28 11 | * 2020-02-23 https://twitter.com/ProjektMelody/status/1231617093642182656 12 | * 2020-05-31 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /misc/projektmelody-torrents.md: -------------------------------------------------------------------------------- 1 | magnet:?xt=urn:btih:Y6SW5PJCUNWNW3ESTJAZSZI4VTVEUPHL&dn=Projektmelody&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce 2 | magnet:?xt=urn:btih:25POG7T3EBJBTRQOZYY6TYZWHWGS4LHC&dn=ProjektMelody%20Stream%20Archive%2027-03-2020&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce 3 | magnet:?xt=urn:btih:DE5ZRE2ZNFT6ERBHZFSLNGUZLDFUVHCV&dn=ProjektMelody%20-%20Archive%2015-02-2019&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce 4 | magnet:?xt=urn:btih:QZHJIQ5VRQ4IGKRPQAEVMD3NP5UAEJ7P&dn=Projektmelody&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce 5 | magnet:?xt=urn:btih:864e9443b58c38832a2f8009560f6d7f680227ef&dn=Projektmelody&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2F9.rarbg.to%3A2710%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce&tr=udp%3A%2F%2Fretracker.lanta-net.ru%3A2710%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.moeking.me%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fipv4.tracker.harry.lu%3A80%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce 6 | magnet:?xt=urn:btih:0814d1bd7ebdf112d80cea9732ea837bfa3f0cea&dn=Projektmelody&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2F9.rarbg.to%3A2710%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce&tr=udp%3A%2F%2Fretracker.lanta-net.ru%3A2710%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.moeking.me%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fipv4.tracker.harry.lu%3A80%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "futureporn", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "description": "https://futureporn.net", 6 | "scripts": { 7 | "build": "npx webpack --mode=production && npm:build:site", 8 | "build:site": "npx @11ty/eleventy --config ./.eleventy.cjs" 9 | }, 10 | "repository": "git+https://github.com/insanity54/futureporn.git", 11 | "author": "Chris Grimmett ", 12 | "license": "Unlicense", 13 | "bugs": { 14 | "url": "https://github.com/insanity54/futureporn/issues" 15 | }, 16 | "homepage": "https://github.com/insanity54/futureporn#readme", 17 | "private": true, 18 | "main": "index.js", 19 | "workspaces": [ 20 | "packages/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/auth/index.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import fastify$0 from "fastify"; 3 | import oauthPlugin from "@fastify/oauth2"; 4 | dotenv.config() 5 | 6 | const fastify = fastify$0({ logger: { level: 'trace' } }); 7 | 8 | 9 | fastify.register(oauthPlugin, { 10 | name: 'patreonOAuth2', 11 | scope: ['identity.memberships'], 12 | credentials: { 13 | client: { 14 | id: process.env.PATREON_CLIENT_ID, 15 | secret: process.env.PATREON_CLIENT_SECRET 16 | }, 17 | auth: { 18 | authorizeHost: 'https://www.patreon.com', 19 | authorizePath: '/oauth2/authorize', 20 | tokenHost: 'https://www.patreon.com', 21 | tokenPath: '/api/oauth2/token' 22 | } 23 | }, 24 | // register a fastify url to start the redirect flow 25 | startRedirectPath: '/login/patreon', 26 | // patreon redirect here after the user login 27 | callbackUri: 'https://rmz78y.tunnel.pyjam.as/login/patreon/callback' 28 | }) 29 | 30 | fastify.get('/login/patreon/callback', async function (request, reply) { 31 | const { token } = await this.patreonOAuth2.getAccessTokenFromAuthorizationCodeFlow(request) 32 | 33 | console.log(token.access_token) 34 | 35 | // if later you need to refresh the token you can use 36 | // const { token: newToken } = await this.getNewAccessTokenUsingRefreshToken(token.refresh_token) 37 | 38 | reply.send({ access_token: token.access_token }) 39 | 40 | // 41 | }) 42 | 43 | fastify.get('/', (req, rep) => { 44 | rep.type('text/html') 45 | rep.send(`

Login with Patreon

`) 46 | }) 47 | 48 | fastify.listen({ 49 | port: process.env.PORT || 3000, 50 | host: '0.0.0.0' 51 | }) 52 | 53 | -------------------------------------------------------------------------------- /packages/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "1.0.0", 4 | "description": "auth server for futureporn.net-- handles patreon oauth login", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "run-p dev:index dev:tunnel", 9 | "dev:index": "PORT=8030 nodemon index", 10 | "start": "node index", 11 | "dev:tunnel": "run-s dev:tunnel:up dev:tunnel:reminder || run-s dev:tunnel:down", 12 | "dev:tunnel:create": "curl https://tunnel.pyjam.as/8030 > tunnel.conf", 13 | "dev:tunnel:reminder": "echo \"remember to run 'dev:tunnel:down' when you're done with development.\"", 14 | "dev:tunnel:up": "wg-quick up ./tunnel.conf", 15 | "dev:tunnel:down": "wg-quick down ./tunnel.conf" 16 | }, 17 | "private": true, 18 | "license": "Unlicense", 19 | "type": "module", 20 | "dependencies": { 21 | "@fastify/oauth2": "^7.0.0", 22 | "fastify": "^4.13.0", 23 | "npm-run-all": "^4.1.5" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^2.0.20" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/auth/tunnel.conf: -------------------------------------------------------------------------------- 1 | 2 | [Interface] 3 | Address = 10.101.0.132/32 4 | PrivateKey = gP11WoDB8SXYYeYseSv4Ilzg7ZsvzzbHTvUFPhpwJlI= 5 | PostUp = printf 'You can now access http://0.0.0.0:8030 on https://rmz78y.tunnel.pyjam.as/ ✨\n' 6 | 7 | [Peer] 8 | PublicKey = n2JMHmCZRcF+eb4iW/bkCP9bJHjxkSxRTR30b0Ghbi4= 9 | AllowedIPs = 10.101.0.1/32 10 | Endpoint = tunnel.pyjam.as:55555 11 | PersistentKeepalive = 21 12 | -------------------------------------------------------------------------------- /packages/builder/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | 3 | .11ty-vite 4 | 5 | # it's important that .cache stay .gitignore, as 6 | # eleventy-fetch is used to cache patreon API requests 7 | .cache/ 8 | 9 | .editorconfig 10 | 11 | 12 | 13 | *.key 14 | 15 | *~ 16 | 17 | # test VOD 18 | website/vods/3021-10-16T00\:30\:00.000Z.md 19 | website/vods/30211015T173000Z.md 20 | 21 | .env 22 | 23 | 24 | 25 | ### Node ### 26 | # Logs 27 | logs 28 | *.log 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | lerna-debug.log* 33 | .pnpm-debug.log* 34 | 35 | 36 | # Runtime data 37 | pids 38 | *.pid 39 | *.seed 40 | *.pid.lock 41 | 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | 48 | 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | .env.production 61 | 62 | 63 | # yarn berry 64 | .pnp.* 65 | .yarn/* 66 | !.yarn/patches 67 | !.yarn/plugins 68 | !.yarn/releases 69 | !.yarn/sdks 70 | !.yarn/versions 71 | -------------------------------------------------------------------------------- /packages/builder/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | 4 | -------------------------------------------------------------------------------- /packages/builder/.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /packages/builder/.pnpmfile.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | readPackage: (pkg) => { 4 | if (pkg.name === "winston") { 5 | pkg.dependencies['logform'] = '^5.2.1'; 6 | } 7 | return pkg; 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/builder/_website/404.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | permalink: 404.html 4 | eleventyExcludeFromCollections: true 5 | --- 6 | 7 |
8 |

404

9 |

Content not found!

10 | 11 |

Go home.

12 |
13 | -------------------------------------------------------------------------------- /packages/builder/_website/_data/.gitignore: -------------------------------------------------------------------------------- 1 | redacted.json -------------------------------------------------------------------------------- /packages/builder/_website/_data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/_website/_data/.gitkeep -------------------------------------------------------------------------------- /packages/builder/_website/_data/contributors.json: -------------------------------------------------------------------------------- 1 | { 2 | "note": "This is a list of people who have contributed in some way. Most commonly, the contribution is in the form of sending a higher quality VOD than what was previously on Futureporn.", 3 | "list": [ 4 | { 5 | "name": "Zipdox", 6 | "link": "https://www.reddit.com/user/Zipdox/" 7 | }, 8 | { 9 | "name": "SamuraiZergling" 10 | }, 11 | { 12 | "name": "BHelp" 13 | }, 14 | { 15 | "name": "anonymous" 16 | }, 17 | { 18 | "name": "NiuIntern" 19 | }, 20 | { 21 | "name": "Jake Well", 22 | "link": "https://twitter.com/NwiiKYAmm" 23 | }, 24 | { 25 | "name": "Awesomeness4512", 26 | "link": "https://www.reddit.com/user/Awesomeness4512" 27 | }, 28 | { 29 | "name": "Fontana Tessitura", 30 | "link": "https://discord.com/invite/fdPTFA6Bx5" 31 | }, 32 | { 33 | "name": "Matrix" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /packages/builder/_website/_data/db.cjs.old: -------------------------------------------------------------------------------- 1 | require('dotenv/config') 2 | // const EleventyFetch = require("@11ty/eleventy-fetch"); 3 | const postgres = require('postgres') 4 | const { format } = require('date-fns') 5 | const dateFnsTz = require('date-fns-tz'); 6 | 7 | const sql = postgres({ 8 | username: process.env.POSTGRES_USERNAME, 9 | password: process.env.POSTGRES_PASSWORD, 10 | host: process.env.POSTGRES_HOST, 11 | idle_timeout: 1 12 | }) 13 | 14 | 15 | module.exports = async function() { 16 | const vods = await sql`SELECT * FROM vod ORDER BY date ASC;` 17 | return { vods } 18 | }; 19 | -------------------------------------------------------------------------------- /packages/builder/_website/_data/donors.json: -------------------------------------------------------------------------------- 1 | { 2 | "note": "This is a list of donors who have donated directly.", 3 | "list": [ 4 | { 5 | "username": "Kiwisama38" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/builder/_website/_data/env.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | STRAPI_BACKEND_URL: (process.env.ELEVENTY_RUN_MODE === 'serve') ? 'http://localhost:1337' : 'https://portal.futureporn.net', 4 | ELEVENTY_RUN_MODE: process.env.ELEVENTY_RUN_MODE, 5 | COMPANION_URL: (process.env.ELEVENTY_RUN_MODE === 'serve') ? 'http://localhost:5000' : 'https://uppy.futureporn.net' 6 | } 7 | }; -------------------------------------------------------------------------------- /packages/builder/_website/_data/gateways.cjs: -------------------------------------------------------------------------------- 1 | // a list of ipfs gateways which a visitor can choose from 2 | 3 | const debug = require('debug')('futureporn') 4 | 5 | const EleventyFetch = require("@11ty/eleventy-fetch"); 6 | 7 | 8 | const futurepornExclusiveGateways = [ 9 | { 10 | hostname: 'gw.futureporn.net', 11 | pattern: 'https://gw.futureporn.net/ipfs/:hash', 12 | note: 'patrons only' 13 | } 14 | ] 15 | 16 | const publicGatewayNames = [ 17 | 'https://ipfs.io/ipfs/:hash', 18 | 'https://:hash.ipfs.dweb.link', 19 | ] 20 | 21 | 22 | 23 | module.exports = async function() { 24 | 25 | // I'm disabling the option to fetch the public gateways because I don't think it's useful 26 | // we need FAST options only, otherwise it's pointless to give the user an option 27 | // 28 | // const publicGatewayNames = await EleventyFetch('https://github.com/ipfs/public-gateway-checker/raw/master/src/gateways.json', { 29 | // duration: '1w', 30 | // type: 'json' 31 | // }) 32 | 33 | const publicGateways = publicGatewayNames 34 | .map((gw) => ({ 35 | pattern: gw, 36 | hostname: new URL(gw.replace(':hash.', '')).hostname 37 | })) 38 | 39 | 40 | return { 41 | free: publicGateways, 42 | premium: futurepornExclusiveGateways 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /packages/builder/_website/_data/github.cjs: -------------------------------------------------------------------------------- 1 | require('dotenv/config') 2 | const EleventyFetch = require("@11ty/eleventy-fetch"); 3 | 4 | 5 | module.exports = async function() { 6 | const incompleteGoalsJson = await EleventyFetch(`https://api.github.com/repos/insanity54/futureporn/issues?labels=Goal&state=open`, { 7 | duration: '1m', 8 | type: 'json' 9 | }) 10 | 11 | const completedGoalsJson = await EleventyFetch('https://api.github.com/repos/insanity54/futureporn/issues?labels=Goal&state=closed', { 12 | duration: '1m', 13 | type: 'json' 14 | }) 15 | 16 | const completeGoals = completedGoalsJson.sort((a, b) => a.updated_at - b.updated_at) 17 | const incompleteGoals = incompleteGoalsJson.sort((a, b) => a.comments - b.comments) 18 | 19 | 20 | return { 21 | goals: { 22 | complete: completeGoals, 23 | incomplete: incompleteGoals 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /packages/builder/_website/_data/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "🔞💦 Futureporn.net", 3 | "url": "https://futureporn.net/", 4 | "description": "ProjektMelody fan site focused on long term preservation of her Chaturbate VODs", 5 | "feed": { 6 | "subtitle": "ProjektMelody fan site.", 7 | "filename": "feed.xml", 8 | "path": "/feed/feed.xml", 9 | "id": "https://futureporn.net/" 10 | }, 11 | "jsonfeed": { 12 | "path": "/feed/feed.json", 13 | "url": "https://futureporn.net/" 14 | }, 15 | "author": { 16 | "name": "Chris Grimmett", 17 | "email": "chris@grimtech.net", 18 | "url": "https://grimtech.net/about" 19 | }, 20 | "goals": [ 21 | { 22 | "amount_cents": 13000, 23 | "completed_percentage": 99, 24 | "description": "This goal would cover my minimum operating cost of Futureporn.net.", 25 | "title": "" 26 | }, 27 | { 28 | "amount_cents": 23000, 29 | "completed_percentage": 56, 30 | "description": "This goal would improve Futureporn video serving speed by paying for the bandwidth required to run a Futureporn IPFS gateway.
", 31 | "title": "" 32 | } 33 | ], 34 | "incompleteGoals": [ 35 | { 36 | "amount_cents": 13000, 37 | "completed_percentage": 99, 38 | "description": "This goal would cover my minimum operating cost of Futureporn.net.", 39 | "title": "" 40 | }, 41 | { 42 | "amount_cents": 23000, 43 | "completed_percentage": 56, 44 | "description": "This goal would improve Futureporn video serving speed by paying for the bandwidth required to run a Futureporn IPFS gateway.
", 45 | "title": "" 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/alpine/components/auth.js: -------------------------------------------------------------------------------- 1 | export default function auth () { 2 | return { 3 | open: false, 4 | accessToken: '', 5 | error: '', 6 | done: false, 7 | async init() { 8 | try { 9 | this.getAccessToken() 10 | await this.getJwt() 11 | this.redirect() 12 | } catch (e) { 13 | this.error = e 14 | } 15 | }, 16 | getAccessToken() { 17 | // greets https://stackoverflow.com/a/901144/1004931 18 | const params = new Proxy(new URLSearchParams(window.location.search), { 19 | get: (searchParams, prop) => searchParams.get(prop), 20 | }); 21 | if (params?.access_token === undefined || params?.access_token === null) { 22 | throw new Error('Failed to get access_token from auth portal.'); 23 | } 24 | else { 25 | this.accessToken = params.access_token 26 | } 27 | }, 28 | async getJwt() { 29 | const res = await fetch(`${this.$refs.backend.innerHTML}/api/auth/patreon/callback?access_token=${this.accessToken}`) 30 | const json = await res.json() 31 | if (json?.jwt === undefined) throw new Error('Failed to get auth token. Please try again later.'); 32 | else { 33 | console.log(JSON.stringify(json)) 34 | if (json?.jwt) Alpine.store('auth').jwt = json.jwt; 35 | if (json?.user?.username) Alpine.store('auth').username = json.user.username; 36 | if (json?.user?.id) Alpine.store('auth').userId = json.user.id; 37 | if (json?.role?.type) Alpine.store('user').role = json.role.type; 38 | } 39 | }, 40 | redirect() { 41 | // this is for redirecting 42 | // from /connect/patreon/redirect 43 | // to whatever page the user was previously at 44 | window.location.pathname = Alpine.store('auth').lastVisitedPath 45 | this.done = true 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/alpine/components/user.js: -------------------------------------------------------------------------------- 1 | export default function user () { 2 | return { 3 | avatar: this.$persist(''), 4 | vanityLink: '', 5 | patreonBenefits: [], 6 | isNamePublic: false, 7 | isLinkPublic: false, 8 | isLoading: false, 9 | isSuccess: false, 10 | isDirty: false, 11 | hasUrlBenefit: false, 12 | isPatron () { 13 | return (Alpine.store('user').role === 'patron' ? true : false) 14 | }, 15 | init () { 16 | this.fetchUser() 17 | }, 18 | get hasUrlBenefit () { 19 | return this.patreonBenefits.includes('10663202') // "Your URL displayed on Futureporn.net" 20 | }, 21 | async fetchUser () { 22 | const res = await fetch(`${Alpine.store('env').backend}/api/profile/me`, { 23 | method: 'GET', 24 | headers: { 25 | 'Authorization': `Bearer ${Alpine.store('auth').jwt}` 26 | } 27 | }) 28 | const json = await res.json() 29 | console.log('here is the user json') 30 | console.log(json) 31 | Alpine.store('user').id = (!!json?.id) ? json.id : 0 32 | Alpine.store('user').role = (!!json?.role?.type) ? json.role.type : 'public' 33 | this.username = json.username 34 | this.isNamePublic = json.isNamePublic || false 35 | this.isPatron = (json?.role?.type === 'patron') ? true : false 36 | this.patreonBenefits = (json?.patreonBenefits) ? json.patreonBenefits.split(',') : [] 37 | }, 38 | async updateUserProfile () { 39 | this.isLoading = true 40 | const res = await fetch(`${Alpine.store('env').backend}/api/profile/${Alpine.store('user').id}`, { 41 | method: 'PUT', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | 'Authorization': `Bearer ${Alpine.store('auth').jwt}` 45 | }, 46 | body: JSON.stringify({ 47 | isNamePublic: this.isNamePublic, 48 | isLinkPublic: this.isLinkPublic, 49 | vanityLink: this.vanityLink 50 | }) 51 | }) 52 | this.isLoading = false 53 | this.isDirty = true 54 | if (!res.ok) { 55 | this.isSuccess = false 56 | } else { 57 | this.isSuccess = true 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/builder/_website/_includes/alpine/stores/auth.js: -------------------------------------------------------------------------------- 1 | // greets https://github.com/horikeso/alpinejs-website-with-authentication-example/blob/main/src/stores/auth.js 2 | 3 | export default function registerAuthStore(alpine) { 4 | alpine.store('auth', { 5 | jwt: alpine.$persist(''), 6 | username: alpine.$persist(''), 7 | userId: alpine.$persist(''), 8 | lastVisitedPath: alpine.$persist('/profile'), 9 | logout () { 10 | alpine.store('auth').jwt = ''; 11 | alpine.store('auth').username = ''; 12 | alpine.store('auth').userId = ''; 13 | }, 14 | login () { 15 | alpine.store('auth').lastVisitedPath = window.location.pathname 16 | const connectUrl = `${window.backend}/api/connect/patreon` 17 | console.log(`we are logging in to ${connectUrl} and the lastVisitedPath is ${alpine.store('auth').lastVisitedPath}`) 18 | window.location.assign(connectUrl) 19 | } 20 | }) 21 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/alpine/stores/env.js: -------------------------------------------------------------------------------- 1 | 2 | export default function registerEnvStore(alpine) { 3 | alpine.store('env', { 4 | backend: window.backend, // can't access $refs at this point 5 | loginUrl: `${window.backend}/api/connect/patreon`, 6 | companionUrl: window.companionUrl 7 | }) 8 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/alpine/stores/player.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function registerPlayerStore(alpine) { 4 | alpine.store('player', { 5 | seconds: 0, 6 | formatTime(seconds) { 7 | return new Date(seconds * 1000).toISOString().slice(11, 19); 8 | } 9 | }) 10 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/alpine/stores/user.js: -------------------------------------------------------------------------------- 1 | 2 | export default function registerUserStore(alpine) { 3 | alpine.store('user', { 4 | id: alpine.$persist(0), 5 | role: alpine.$persist('public'), 6 | image: alpine.$persist('https://placehold.co/32x32') 7 | }) 8 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/fundingGoal.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | {# display the last funded goal and the first unfunded goal #} 4 | 5 | 6 |
7 |
8 | Funding Goal 9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 | {% set fundedGoal = patreon.goals.complete[patreon.goals.complete.length-1] %} 20 |

${{ (fundedGoal.amount_cents*(fundedGoal.completed_percentage*0.01)/100) | round }} of ${{ fundedGoal.amount_cents / 100 }} ({{ fundedGoal.completed_percentage}}%)

21 | 22 |
FUNDED
23 | 24 |

{{ fundedGoal.description }}

25 |
26 | 27 | 28 | 29 |
30 | 31 | {% set unfundedGoal = patreon.goals.incomplete[0] %} 32 |

${{ (unfundedGoal.amount_cents*(unfundedGoal.completed_percentage*0.01)/100) | round }} of ${{ unfundedGoal.amount_cents / 100 }} ({{ unfundedGoal.completed_percentage}}%)

33 | 34 | {{ unfundedGoal.completed_percentage }}% 35 | 36 |

{{ unfundedGoal.description }}

37 |
38 |
39 | 40 |

Thank you, 41 | 42 | Patrons! 43 | 44 |

45 |
46 |
47 | -------------------------------------------------------------------------------- /packages/builder/_website/_includes/heading.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Unofficial ProjektMelody Chaturbate VOD Archive. For Adults Only. (NSFW)

5 |
6 |
7 |
-------------------------------------------------------------------------------- /packages/builder/_website/_includes/js/base.js: -------------------------------------------------------------------------------- 1 | 2 | window.backend = document.querySelector('#backend-url').innerHTML 3 | window.companionUrl = document.querySelector('#companion-url').innerHTML 4 | 5 | import Alpine from 'alpinejs' 6 | import persist from '@alpinejs/persist' 7 | import registerEnvStore from '/@includes/alpine/stores/env.js' 8 | import registerAuthStore from '/@includes/alpine/stores/auth.js' 9 | import registerUserStore from '/@includes/alpine/stores/user.js' 10 | import registerPlayerStore from '/@includes/alpine/stores/player.js' 11 | import auth from '/@includes/alpine/components/auth.js' 12 | import player from '/@includes/alpine/components/player.js' 13 | import user from '/@includes/alpine/components/user.js' 14 | import upload from '/@includes/alpine/components/upload.js' 15 | import tagger from '/@includes/alpine/components/tagger.js' 16 | 17 | Alpine.plugin(persist) 18 | registerEnvStore(Alpine) 19 | registerAuthStore(Alpine) 20 | registerUserStore(Alpine) 21 | registerPlayerStore(Alpine) 22 | 23 | window.auth = auth; 24 | window.player = player; 25 | window.upload = upload; 26 | window.user = user; 27 | window.tagger = tagger; 28 | window.Alpine = Alpine; 29 | 30 | queueMicrotask(() => { 31 | Alpine.start(); 32 | }); 33 | 34 | 35 | // handler for the burger menu 36 | document.addEventListener('DOMContentLoaded', () => { 37 | 38 | // Get all "navbar-burger" elements 39 | const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); 40 | 41 | // Add a click event on each of them 42 | $navbarBurgers.forEach( el => { 43 | el.addEventListener('click', () => { 44 | 45 | // Get the target from the "data-target" attribute 46 | const target = el.dataset.target; 47 | const $target = document.getElementById(target); 48 | 49 | // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" 50 | el.classList.toggle('is-active'); 51 | $target.classList.toggle('is-active'); 52 | 53 | }); 54 | }); 55 | 56 | }); 57 | 58 | 59 | -------------------------------------------------------------------------------- /packages/builder/_website/_includes/js/common.js: -------------------------------------------------------------------------------- 1 | 2 | const defaultPattern = '' 3 | 4 | export function buildIpfsUrl (pattern, cid) { 5 | pattern = pattern || defaultPattern; 6 | if (!cid) return ''; 7 | const output = pattern.replace(':hash', cid) 8 | return output 9 | } 10 | -------------------------------------------------------------------------------- /packages/builder/_website/_includes/js/profile.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/_website/_includes/js/profile.js -------------------------------------------------------------------------------- /packages/builder/_website/_includes/layouts/profile.njk: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.njk" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | 7 | 8 | {% block content %} 9 | {{ content | safe }} 10 | {% endblock %} 11 | 12 | 13 | {% block footer %} 14 | {{ super() }} 15 | {% endblock %} 16 | 17 | {% block scripts %} 18 | {{ super() }} 19 | 20 | {% endblock %} 21 | 22 | 23 | {% block end %} 24 | {{ super() }} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /packages/builder/_website/_includes/layouts/vod2.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: vod2 3 | --- 4 | 5 |

Vod

6 | 7 | id:{{ vod.id }}
8 | title:{{ vod.title }} -------------------------------------------------------------------------------- /packages/builder/_website/_includes/styles/base.scss: -------------------------------------------------------------------------------- 1 | 2 | @charset "utf-8"; 3 | 4 | 5 | 6 | @import './variables.scss'; 7 | @import '/@root/node_modules/bulma/bulma.sass'; 8 | @import '/@root/node_modules/bulma-prefers-dark/bulma-prefers-dark.sass'; 9 | 10 | 11 | @import "/@root/node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss"; 12 | @import "/@root/node_modules/@fortawesome/fontawesome-free/scss/brands.scss"; 13 | @import "/@root/node_modules/@fortawesome/fontawesome-free/scss/solid.scss"; 14 | 15 | 16 | 17 | html { 18 | background-image: linear-gradient(#1d1058, #1c1a5b); 19 | } 20 | 21 | 22 | div.card-content p { 23 | text-overflow: ellipsis; 24 | overflow: hidden; 25 | height: 1rem; 26 | white-space: nowrap; 27 | } 28 | 29 | a.pagination-link, 30 | a.pagination-next, 31 | a.pagination-previous, 32 | progress.progress, 33 | button.button { 34 | vertical-align: unset; 35 | } 36 | 37 | figure.image { 38 | background-image: linear-gradient(45deg, #1d1058, #7854be); 39 | border-top-left-radius: 0.25rem; 40 | border-top-right-radius: 0.25rem; 41 | } 42 | 43 | figure.is-rounded { 44 | border-radius: 50%; 45 | } 46 | 47 | nav.navbar, .noselect { 48 | user-select: none; 49 | } 50 | 51 | [x-cloak] { display: none !important; } 52 | 53 | 54 | div.tags-input { 55 | user-select: none; 56 | } 57 | 58 | @media (prefers-color-scheme: dark) { 59 | div.tags-input { 60 | background-color: $darkgray; 61 | border-color: hsl(0, 0%, 14%); 62 | color: $gray; 63 | } 64 | } -------------------------------------------------------------------------------- /packages/builder/_website/_includes/styles/player.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/builder/_website/_includes/styles/variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $red: #1d1058; 3 | $darkred: #1c1a5b; 4 | $lightgray: #333333; 5 | $gray: #C0C0C0; 6 | $darkgray: #141414; 7 | $navy: #17050F; 8 | $blue: #082840; 9 | $white: #fff; 10 | $highlight: #6749d7; 11 | $tcolor: #856fd7; 12 | $color1: #7854be; 13 | $color2: #1f3fb6; 14 | $color3: #851095; 15 | $color4: #5d85ca; 16 | $color5: #333d49; 17 | $color6: #27106f; 18 | $wingrey: #bbc3c4; 19 | $turquoise: #00ffcc; 20 | $fa-font-path: '/webfonts'; 21 | $pirmary: $red; -------------------------------------------------------------------------------- /packages/builder/_website/_includes/toyCard.njk: -------------------------------------------------------------------------------- 1 | 2 | {% set toyUrl %}/tags/{{ toy.attributes.tag.data.attributes.name }}/{% endset %} 3 | {% set displayName %}{{ toy.attributes.make }} {{ toy.attributes.model }}{% endset %} 4 |
5 | {% image toy.attributes.image.data.attributes.formats.small.url displayName "" %} 6 |

{{toy.attributes.model}}

7 |
-------------------------------------------------------------------------------- /packages/builder/_website/_includes/vodCard.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 56 | 57 |
58 | 59 | -------------------------------------------------------------------------------- /packages/builder/_website/api/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | eleventyNavigation: 4 | key: API 5 | order: 5 6 | --- 7 | 8 |
9 |
10 |
11 |

Futureporn API

12 |

Application Programmable Interface (API) for developers

13 | 14 |

Data API

15 |

The Data API contains all the data served by this website in JSON format, including IPFS Content IDs (CID), VOD titles, dates, and stream announcement links.

16 |

Futureporn API Version 1

17 | 18 |

IPFS Cluster Template

19 |

The IPFS Cluster Template allows other IPFS cluster instances to join the Futureporn.net IPFS cluster as a follower peer. Cluster peers automatically pin (replicate) the IPFS content listed on this website.

20 | 21 |

Basic instructions are as follows

22 |
    23 |
  1. Download & install both ipfs-kubo and ipfs-cluster-follow onto your server.
  2. 24 |
  3. Initialize your ipfs repo & start the ipfs daemon 25 |
    26 | ipfs init
    27 | ipfs daemon
    28 | 
    29 |
  4. 30 |
  5. Join the cluster
    CLUSTER_PEERNAME="replace-me-with-your-super-cool-name" ipfs-cluster-follow futureporn.net run --init https://futureporn.net/api/service.json
  6. 31 |
32 | 33 | 34 |

Futureporn IPFS Cluster Template (service.json)

35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /packages/builder/_website/api/v1.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: false 3 | permalink: '/public/api/v1.json' 4 | --- 5 | { 6 | "vods": [ 7 | {% for vod in db.vods %} 8 | { 9 | "title": "{{ vod.attributes.title | urlencode }}", 10 | "videoSrcHash": "{{ vod.attributes.videoSrcHash }}", 11 | "video720Hash": "{{ vod.attributes.video720Hash }}", 12 | "video480Hash": "{{ vod.attributes.video480Hash }}", 13 | "video360Hash": "{{ vod.attributes.video360Hash }}", 14 | "video240Hash": "{{ vod.attributes.video240Hash }}", 15 | "thinHash": "{{ vod.attributes.thinHash }}", 16 | "thiccHash": "{{ vod.attributes.thiccHash }}", 17 | "announceTitle": "{{ vod.attributes.announceTitle | urlencode }}", 18 | "announceUrl": "{{ vod.attributes.announceUrl }}", 19 | "date": "{{ vod.attributes.date }}", 20 | "note": "{{ vod.attributes.note }}", 21 | "url": "{{ vod.attributes.url }}" 22 | }{% if not loop.last %},{% endif %} 23 | {% endfor %} 24 | ] 25 | } -------------------------------------------------------------------------------- /packages/builder/_website/connect/patreon/redirect.njk: -------------------------------------------------------------------------------- 1 | 2 | {# 3 | After user auths, 4 | they are redirected to this page. 5 | 6 | This page grabs the access_token from the query string, 7 | exchanges it with strapi for a jwt 8 | then persists the jwt 9 | 10 | After a jwt is stored, this page redirects the user to whatever page they were previously on. 11 | 12 | #} 13 | 14 | 15 | 16 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | {{ env.STRAPI_BACKEND_URL }} 41 | {{ env.COMPANION_URL }} 42 | 43 |
44 |

Redirecting...

45 |
46 | 47 |
48 |

Error

49 |

50 | Home 51 |
52 | 53 |
54 | Click here if you are not automatically redirected 55 |
56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /packages/builder/_website/favicon.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: favicon.ico 3 | eleventyExcludeFromCollections: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/builder/_website/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/_website/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/builder/_website/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/_website/favicon/favicon.ico -------------------------------------------------------------------------------- /packages/builder/_website/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/_website/favicon/favicon.png -------------------------------------------------------------------------------- /packages/builder/_website/feed/feed.xml.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /public/feed/feed.xml 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | {{ metadata.title }} 8 | {{ metadata.description }} 9 | 10 | 11 | {{ metadata.url }} 12 | 13 | {{ metadata.author.name }} 14 | {{ metadata.author.email }} 15 | 16 | {%- for vod in db.vods %} 17 | {% set absolutePostUrl %}{{ vod.attributes.date | safeDate | vodUrl | absoluteUrl(metadata.url) }}{% endset %} 18 | 19 | {{ vod.attributes.title }} 20 | 21 | {{ vod.attributes.date | isoStringToRfc3339 }} 22 | {{ absolutePostUrl }} 23 | {{ vod.attributes.videoSrcHash | buildIpfsUrl }} 24 | 25 |
26 | {%- endfor %} 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/builder/_website/feed/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | eleventyNavigation: 4 | key: Feed 5 | order: 6 6 | --- 7 | 8 | 9 |
10 |
11 |
12 | 13 |

RSS Feed

14 | 15 |

Keep up to date with new VODs using Real Simple Syndication (RSS).

16 | 17 |

Don't have a RSS reader? Futureporn recommends Fraidycat

18 | 19 |

feed.xml

20 |
21 |
22 |
-------------------------------------------------------------------------------- /packages/builder/_website/goals.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | eleventyNavigation: 4 | key: Goals 5 | order: 3 6 | --- 7 | 8 |
9 | 10 |
11 |

Futureporn Goals

12 | 13 |

Goals are listed in random order

14 |
    15 | 16 | {# Most goals are fetched from github #} 17 | {%- for goal in github.goals.incomplete %} 18 |
  • ☐ {{ goal.title }}

  • 19 | {% endfor -%} 20 | 21 | 22 | {# funding goal #} 23 | {% if patreon.goals.complete.length === 0 %} 24 |
  • ☐ Secure website funding

  • 25 | {% endif %} 26 | 27 | {# all vods archived progress #} 28 | {% if not db.vods | isArchiveComplete %} 29 |
  • ☐ Archive every ProjektMelody Chaturbate stream. {% ipfsProgressPercentage db.vods %}

  • 30 | {% endif %} 31 | 32 | {# 240p encode progress #} 33 | {% if not db.vods | is240pComplete %} 34 |
  • ☐ Transcode a 240p version of each stream. {% transcode240pProgressPercentage db.vods %}

  • 35 | {% endif %} 36 | 37 | {# thumbnail progress #} 38 | {% if not db.vods | isThumbnailsComplete %} 39 |
  • ☐ Generate thumbnail for each stream. {% thumbnailProgressPercentage db.vods %}

  • 40 | {% endif %} 41 | 42 |
43 | 44 | 45 | 46 | 47 |

Completed Goals

48 |
    49 | {%- for goal in github.goals.complete %} 50 |
  • ✅ {{ goal.title }}

  • 51 | {% endfor -%} 52 | 53 | 54 | {# funding goal completed #} 55 | {% if patreon.goals.complete.length > 0 %} 56 |
  • ✅ Secure website funding

  • 57 | {% endif %} 58 | 59 | {% if db.vods | isArchiveComplete %} 60 |
  • ✅ Archive every ProjektMelody Chaturbate stream.

  • 61 | {% endif %} 62 | 63 | {# 240p encode progress #} 64 | {% if db.vods | is240pComplete %} 65 |
  • ✅ Transcode a 240p version of each stream.

  • 66 | {% endif %} 67 | 68 | {# thumbnail progress #} 69 | {% if db.vods | isThumbnailsComplete %} 70 |
  • ✅ Generate thumbnail for each stream.

  • 71 | {% endif %} 72 | 73 |
74 |
75 | 76 |
-------------------------------------------------------------------------------- /packages/builder/_website/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | eleventyNavigation: 4 | key: Home 5 | order: 1 6 | pagination: 7 | data: db.vods 8 | size: 48 9 | reverse: true 10 | --- 11 | 12 | {% include "heading.njk" %} 13 | {% include "fundingGoal.njk" %} 14 | 15 | 16 | 17 |
18 | {% asyncEach vod in pagination.items %} 19 | {% include "vodCard.njk" %} 20 | {% endeach %} 21 |
22 | 23 | 24 |
25 | 72 |
73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /packages/builder/_website/patrons.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | eleventyNavigation: 4 | key: Supporters 5 | order: 4 6 | --- 7 | 8 | 9 |
10 |
11 |
12 | 13 |

Patron List

14 |

Futureporn.net continues to improve thanks to {{ patreon.patronCount }} generous supporters.

15 | 16 | {% set donorsAndPatrons = patreon.patrons.concat(donors.list) %} 17 | 18 |
19 | {%- for patron in donorsAndPatrons %} 20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | {% if patron.username %} 28 |

29 | {{ patron.username }} 30 |

31 | {% endif %} 32 | {% if patron.vanityLink %} 33 | 34 | {{ patron.vanityLink }} 35 | 36 | 37 | {% endif %} 38 |
39 |
40 |
41 | 42 |
43 | {% endfor %} 44 |
45 | 46 |

Want to get your name on this list, and get perks like FAST video streaming on select VODs? Become a patron today!

Patron names are private by default-- Opt-in to have your name displayed.

47 | 48 | 49 |
50 | 51 |
52 |
-------------------------------------------------------------------------------- /packages/builder/_website/sitemap.xml.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /public/sitemap.xml 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | {%- for page in collections.all %} 8 | {% set absoluteUrl %}{{ page.url | url | absoluteUrl(metadata.url) }}{% endset %} 9 | 10 | {{ absoluteUrl }} 11 | {{ page.date | htmlDateString }} 12 | 13 | {%- endfor %} 14 | 15 | -------------------------------------------------------------------------------- /packages/builder/_website/tags-list.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /tags/ 3 | layout: layouts/base.njk 4 | --- 5 | 6 |
7 |
8 |

Tags

9 |
10 | {% for tag in db.tags %} 11 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 12 | 13 | {{ tag }} 14 | 15 | {% endfor %} 16 |
17 |
18 |
-------------------------------------------------------------------------------- /packages/builder/_website/tags-pages.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: db.tags 4 | size: 1 5 | alias: tag 6 | filter: 7 | - all 8 | - nav 9 | - vod 10 | - vods 11 | - tagList 12 | addAllPagesToCollections: false 13 | layout: layouts/base.njk 14 | eleventyComputed: 15 | title: Tagged “{{ tag }}” 16 | permalink: /tags/{{ tag | slugify }}/ 17 | --- 18 | 19 | {% set vodslist = db.vods | filterByTag(tag) %} 20 | 21 |
22 |

Tagged “{{ tag }}”

23 |
24 | 25 | 26 |
27 | {% asyncEach vod in vodslist %} 28 | {% include "vodCard.njk" %} 29 | {% endeach %} 30 |
31 | 32 |
33 |

See all tags.

34 |
-------------------------------------------------------------------------------- /packages/builder/_website/upload.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | --- 4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 |

Video Upload

13 | 14 |
15 |
16 |

Metadata

17 |
18 |
19 |
20 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 51 |
52 |
53 | -------------------------------------------------------------------------------- /packages/builder/_website/vod-pages.11ty.js: -------------------------------------------------------------------------------- 1 | 2 | const { format, utcToZonedTime, } = require('date-fns-tz'); 3 | 4 | function safeDate (text) { 5 | const date = utcToZonedTime(text, 'UTC'); 6 | const formattedDate = format(date, "yyyyMMdd'T'HHmmss'Z'", { timezone: 'UTC' }); 7 | return formattedDate; 8 | } 9 | 10 | module.exports = { 11 | data: { 12 | permalink: function (data) { 13 | return `/vods/${safeDate(data.vod.attributes.date)}/`; 14 | }, 15 | layout: 'layouts/vod.njk', 16 | pagination: { 17 | data: 'db.vods', 18 | size: 1, 19 | alias: 'vod' 20 | } 21 | }, 22 | render: data => `

${JSON.stringify(data.vod)}

` 23 | }; -------------------------------------------------------------------------------- /packages/builder/_website/vt/projektmelody/music.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | --- 4 | 5 | 6 |
7 |

Cyberbate 2069 by @LordAethelstan - Sept 24 2021

8 | 9 |

https://twitter.com/LordAethelstan/status/1441480228602204165

10 |

https://twitter.com/ProjektMelody/status/1611104794239959040

11 | 12 | 13 |

Untitled Stream BGM By vanessa barnes - Sept 24 2021

14 | 15 |

https://www.youtube.com/watch?v=CaTwIoJ6nFI

16 |

https://twitter.com/DarylBarnes_/status/1441535024679899148

17 | 18 | 19 |

Jazzy eurobeat by Daryl Vanes Barnes - Sept 24 2021

20 | 21 |

https://twitter.com/DarylBarnes_/status/1548362427439280129

22 | 23 | 24 |

Future Pop Cute 1 by ??? - ???

25 | 26 |

https://www.youtube.com/watch?v=BL_0bRIJ5RE

27 | 28 | 29 | 30 |

Projekt Melody - Melody's Theme - DAYMARE: Dimension Wars Music Extended - 2021

31 | 32 |

https://www.youtube.com/watch?v=FesGItcGxQM

33 | 34 | 35 | 36 |

Projekt melody Intro song "I CAN SHOW YOU" (ARGOFOX RELEASE) BY ECCENTRIC. - 2020

37 | 38 |

https://www.youtube.com/watch?v=wPk7uqgohEw

39 |

https://www.youtube.com/watch?v=U8qV6bOte1A

40 | 41 |

MoanPunk OST from (A Nut Between Worlds)

42 | 43 |

https://www.youtube.com/watch?v=KRwWIPHCy4w

44 | 45 | 46 |

Original BGM by ISAAK_WOLF

47 | 48 |

https://youtu.be/9iqVYFEEnTw

49 |

https://twitter.com/isaak_wolf

50 | 51 | 52 |

references

53 | 54 |

Sept 24 2021 tweet showing BGM artist credits

55 | 56 |

https://twitter.com/DarylBarnes_/status/1441498779656470540

57 | 58 | 59 |

ProjektMelody's Official Playlist

60 | 61 |

https://open.spotify.com/playlist/4DLyjRH9elAil3nXGatCcY?si=AjEe8EyVR9u2-fhZEU75sw

62 | 63 | 64 |
-------------------------------------------------------------------------------- /packages/builder/_website/vt/projektmelody/toy-list.md: -------------------------------------------------------------------------------- 1 | lovense-lush 2 | fantasy-for-her-suction 3 | milking-device 4 | clit-milking-device 5 | crave-vesper-necklace 6 | womanzer-duo 7 | lovense-hyphy 8 | handcuffs 9 | nipple-clamps 10 | hitachi-magic-wand 11 | strapon-dildo 12 | sexmachine 13 | lovense-sex-machine 14 | frickenator 15 | love-partner-piston-dildo-harness 16 | banshee (tag needed) 17 | glass-dildo 18 | splorch (tag needed) 19 | lawnmower 20 | motorbunny 21 | lovense-hush 22 | chastity-device 23 | ball-gag 24 | collar 25 | labia-spreader 26 | lovense-osci 27 | shibari 28 | bad-dragon-tako (she hasn't used it) 29 | monster-dick (ambiguous tag) 30 | spanking-paddle (tag needed) 31 | lovense-ferri (tag needed) 32 | lovense-domi (tag needed) 33 | 34 | 35 | https://www.reddit.com/r/projektmelody/comments/10nghmr/which_streams_are_the_ones_where_she_uses_a_new/ -------------------------------------------------------------------------------- /packages/builder/netlify.toml: -------------------------------------------------------------------------------- 1 | [[plugins]] 2 | package = "netlify-plugin-cache" 3 | 4 | [plugins.inputs] 5 | paths = [ 6 | "/.img-cache", # Eleventy Image Disk Cache 7 | ".cache" # Remote Asset Cache 8 | ] -------------------------------------------------------------------------------- /packages/builder/public/img/cj_clippy.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/img/cj_clippy.jpeg -------------------------------------------------------------------------------- /packages/builder/public/img/cj_clippy_sm.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/img/cj_clippy_sm.jpeg -------------------------------------------------------------------------------- /packages/builder/public/img/gwfuturepornnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/img/gwfuturepornnet.png -------------------------------------------------------------------------------- /packages/builder/public/img/projekt-melody.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/img/projekt-melody.jpg -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /packages/builder/public/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/builder/public/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /packages/builder/removeUrlEncodedInputPaths.js: -------------------------------------------------------------------------------- 1 | // this is a workaround for https://github.com/slinkity/slinkity/issues/240 2 | // greets ChatGPT v4 3 | 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | console.log(`[removeUrlEncodedInputPaths.js] -- workaround for https://github.com/slinkity/slinkity/issues/240`) 9 | 10 | 11 | function replaceAllInstances(dirPath) { 12 | fs.readdir(dirPath, (err, files) => { 13 | if (err) { 14 | console.error(err); 15 | return; 16 | } 17 | 18 | files.forEach((file) => { 19 | const filePath = path.join(dirPath, file); 20 | fs.stat(filePath, (err, stat) => { 21 | if (err) { 22 | console.error(err); 23 | return; 24 | } 25 | 26 | if (stat.isDirectory()) { 27 | replaceAllInstances(filePath); 28 | } else { 29 | const extension = path.extname(filePath).toLowerCase(); 30 | if (extension === '.html' || extension === '.js') { 31 | let newName = file.replace(/%2F/g, '_'); 32 | if (newName !== file) { 33 | const newFilePath = path.join(dirPath, newName); 34 | fs.rename(filePath, newFilePath, (err) => { 35 | if (err) { 36 | console.error(err); 37 | return; 38 | } 39 | processFile(newFilePath); 40 | }); 41 | } else { 42 | processFile(filePath); 43 | } 44 | } 45 | } 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | function processFile(filePath) { 52 | fs.readFile(filePath, 'utf8', (err, data) => { 53 | if (err) { 54 | console.error(err); 55 | return; 56 | } 57 | 58 | const newData = data.replace(/%2F/g, '_'); 59 | fs.writeFile(filePath, newData, 'utf8', (err) => { 60 | if (err) { 61 | console.error(err); 62 | } 63 | }); 64 | }); 65 | } 66 | 67 | // Usage: node app.js directory_path 68 | const dirPath = process.argv[2]; 69 | if (!dirPath) { 70 | console.error('Please provide a directory path.'); 71 | process.exit(1); 72 | } 73 | 74 | replaceAllInstances(dirPath); 75 | -------------------------------------------------------------------------------- /packages/builder/vite.config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default { 4 | appType: "mpa", 5 | server: { 6 | mode: 'development', 7 | middlewareMode: true 8 | }, 9 | build: { 10 | mode: "production" 11 | }, 12 | plugins: [ 13 | 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/capture/README.md: -------------------------------------------------------------------------------- 1 | # Capture 2 | 3 | ## Dev notes 4 | 5 | ### youtube-dl end of stream output 6 | 7 | ``` 8 | [https @ 0x5646887f1580] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/chunklist_w881713853_b5128000_t64RlBTOjMwLjA=.m3u8' for reading 9 | [hls @ 0x564687dd0980] Skip ('#EXT-X-VERSION:4') 10 | [hls @ 0x564687dd0980] Skip ('#EXT-X-DISCONTINUITY-SEQUENCE:0') 11 | [hls @ 0x564687dd0980] Skip ('#EXT-X-PROGRAM-DATE-TIME:2023-01-31T17:48:45.947+00:00') 12 | [https @ 0x5646880bf880] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/media_w881713853_b5128000_t64RlBTOjMwLjA=_18912.ts' for reading 13 | [https @ 0x564688097d00] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/media_w881713853_b5128000_t64RlBTOjMwLjA=_18913.ts' for reading 14 | [https @ 0x5646887f1580] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/chunklist_w881713853_b5128000_t64RlBTOjMwLjA=.m3u8' for reading 15 | [https @ 0x5646886e8580] HTTP error 403 Forbidden 16 | [hls @ 0x564687dd0980] keepalive request failed for 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/chunklist_w881713853_b5128000_t64RlBTOjMwLjA=.m3u8' with error: 'Server returned 403 Forbidden (access denied)' when parsing playlist 17 | [https @ 0x5646886ccfc0] HTTP error 403 Forbidden 18 | [hls @ 0x564687dd0980] Failed to reload playlist 0 19 | [https @ 0x5646886bf680] HTTP error 403 Forbidden 20 | [hls @ 0x564687dd0980] Failed to reload playlist 0 21 | frame= 5355 fps= 31 q=-1.0 Lsize= 71404kB time=00:02:58.50 bitrate=3277.0kbits/s speed=1.02x 22 | video:68484kB audio:2790kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.181873% 23 | [ffmpeg] Downloaded 73117881 bytes 24 | [download] 100% of 69.73MiB in 02:57 25 | ``` -------------------------------------------------------------------------------- /packages/capture/integration/Ipfs.test.js: -------------------------------------------------------------------------------- 1 | 2 | import Ipfs from '../src/Ipfs.js' 3 | import { expect } from 'chai' 4 | import path, { dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | const ipfsExecutable = '/home/chris/.local/bin/ipfs' 9 | 10 | describe('Ipfs', function() { 11 | describe('hash', function () { 12 | it('should hash a file and return the v1 CID', async function () { 13 | const ipfs = new Ipfs({ ipfsExecutable }) 14 | const cid = await ipfs.hash(path.join(__dirname, '../test/fixtures/mock-stream0.mp4')) 15 | expect(cid).to.equal('bafkreihfbftehabfrakhr6tmbx72inewwpayw6cypwgm6lbhbf7mxm7wni') 16 | }) 17 | }) 18 | }) -------------------------------------------------------------------------------- /packages/capture/integration/Voddo.test.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import Voddo from '../src/Voddo.js' 3 | import { 4 | expect 5 | } from 'chai' 6 | import sinon from 'sinon' 7 | import YoutubeDlWrap from 'youtube-dl-wrap' 8 | import { 9 | EventEmitter 10 | } from 'events' 11 | import { getRandomRoom } from '../src/cb.js' 12 | import path, { dirname } from 'path'; 13 | import { fileURLToPath } from 'url'; 14 | const __dirname = dirname(fileURLToPath(import.meta.url)); 15 | 16 | 17 | describe('voddo', function() { 18 | 19 | 20 | describe('getVideoLength', function () { 21 | it('should return the video length in ms', async function () { 22 | const fixtureFile = path.join(__dirname, '..', 'test', 'fixtures', 'mock-stream0.mp4') 23 | const length = await Voddo.getVideoLength(fixtureFile) 24 | expect(length).to.equal(3819) 25 | }) 26 | }) 27 | 28 | it('aborted stream', function(done) { 29 | this.timeout(10000) 30 | 31 | getRandomRoom().then((room) => { 32 | console.log(room) 33 | const abortController = new AbortController() 34 | 35 | const url = `https://chaturbate.com/${room}` 36 | const format = 'best' 37 | const cwd = '/tmp' 38 | const voddo = new Voddo({ 39 | url, 40 | format, 41 | cwd 42 | }) 43 | 44 | 45 | voddo.once('stop', function(data) { 46 | console.log('f in chat') 47 | expect(voddo.stats.files[0]).to.have.property('size') 48 | done() 49 | }) 50 | 51 | voddo.start() 52 | 53 | setTimeout(() => { 54 | voddo.stop() 55 | }, 5000) 56 | }) 57 | 58 | 59 | }) 60 | 61 | 62 | }) -------------------------------------------------------------------------------- /packages/capture/integration/video.test.js: -------------------------------------------------------------------------------- 1 | 2 | import 'dotenv/config' 3 | import Video from '../src/Video.js' 4 | import { expect } from 'chai' 5 | import { dirname } from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | import path from 'node:path' 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | const dataFixture = [ 12 | { 13 | timestamp: 1, 14 | file: 'mock-stream0.mp4' 15 | }, { 16 | timestamp: 2, 17 | file: 'mock-stream1.mp4' 18 | }, { 19 | timestamp: 3, 20 | file: 'mock-stream2.mp4' 21 | } 22 | ] 23 | 24 | xdescribe('video', function () { 25 | describe('concat', function () { 26 | it('should combine several videos into one', async function() { 27 | const cwd = path.join(__dirname, './fixtures') 28 | const outputFile = await concat(dataFixture, { 29 | cwd 30 | }) 31 | }) 32 | }) 33 | }) -------------------------------------------------------------------------------- /packages/capture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "futureporn-capture", 3 | "version": "0.0.11", 4 | "main": "index.js", 5 | "license": "Unlicense", 6 | "private": true, 7 | "type": "module", 8 | "scripts": { 9 | "start": "node --trace-warnings index", 10 | "test": "FUTUREPORN_WORKDIR=/home/chris/Downloads mocha", 11 | "integration": "FUTUREPORN_WORKDIR=/home/chris/Downloads mocha ./integration/**/*.test.js", 12 | "dev": "FUTUREPORN_WORKDIR=/home/chris/Downloads nodemon index" 13 | }, 14 | "dependencies": { 15 | "common": "workspace:*", 16 | "cuid": "^2.1.8", 17 | "dotenv": "^16.0.3", 18 | "execa": "^6.1.0", 19 | "fastify": "^4.12.0", 20 | "fastq": "^1.15.0", 21 | "faye": "^1.4.0", 22 | "faye-websocket": "^0.11.4", 23 | "fluent-ffmpeg": "^2.1.2", 24 | "https": "^1.0.0", 25 | "ioredis": "^5.2.4", 26 | "minimatch": "^5.1.1", 27 | "p-retry": "^5.1.2", 28 | "postgres": "^3.3.3", 29 | "rxjs": "^7.8.0", 30 | "sql": "^0.78.0", 31 | "youtube-dl-wrap": "git+https://github.com/insanity54/youtube-dl-wrap.git" 32 | }, 33 | "devDependencies": { 34 | "chai": "^4.3.7", 35 | "cheerio": "^1.0.0-rc.12", 36 | "mocha": "^10.2.0", 37 | "multiformats": "^11.0.1", 38 | "node-abort-controller": "^3.0.1", 39 | "node-fetch": "^3.3.0", 40 | "nodemon": "^2.0.20", 41 | "sinon": "^15.0.1", 42 | "sinon-chai": "^3.7.0", 43 | "sinon-test": "^3.1.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/capture/src/Ipfs.js: -------------------------------------------------------------------------------- 1 | 2 | import {execa} from 'execa' 3 | import {loggerFactory} from 'common/logger' 4 | 5 | const logger = loggerFactory({ 6 | service: 'futureporn/capture' 7 | }) 8 | 9 | export default class Ipfs { 10 | constructor(opts) { 11 | this.multiaddr = opts?.IPFS_CLUSTER_HTTP_API_MULTIADDR 12 | this.username = opts?.IPFS_CLUSTER_HTTP_API_USERNAME 13 | this.password = opts?.IPFS_CLUSTER_HTTP_API_PASSWORD 14 | this.ctlExecutable = opts?.ctlExecutable || '/usr/local/bin/ipfs-cluster-ctl' 15 | this.ipfsExecutable = opts?.ipfsExecutable || '/usr/local/bin/ipfs' 16 | } 17 | getArgs () { 18 | let args = [ 19 | '--no-check-certificate', 20 | '--host', this.multiaddr, 21 | '--basic-auth', `${this.username}:${this.password}` 22 | ] 23 | return args 24 | } 25 | async upload (filename, expiryDuration = false) { 26 | try { 27 | let args = getArgs() 28 | 29 | args = args.concat([ 30 | 'add', 31 | '--quieter', 32 | '--cid-version', 1 33 | ]) 34 | 35 | if (expiryDuration) { 36 | args = args.concat(['--expire-in', expiryDuration]) 37 | } 38 | 39 | args.push(filename) 40 | 41 | const { stdout } = await execa(this.ctlExecutable, args) 42 | return stdout 43 | } catch (e) { 44 | logger.log({ level: 'error', message: 'Error while adding file to ipfs' }) 45 | logger.log({ level: 'error', message: e }) 46 | } 47 | } 48 | async hash (filename) { 49 | try { 50 | const { stdout } = await execa(this.ipfsExecutable, ['add', '--quiet', '--cid-version=1', '--only-hash', filename]) 51 | return stdout 52 | } catch (e) { 53 | logger.log({ level: 'error', message: 'Error while hashing file' }) 54 | logger.log({ level: 'error', message: e }) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /packages/capture/src/Video.js: -------------------------------------------------------------------------------- 1 | 2 | import { execa } from 'execa' 3 | import { tmpdir } from 'os' 4 | import path from 'node:path' 5 | import fs from 'node:fs' 6 | import os from 'node:os' 7 | 8 | export class VideoConcatError extends Error { 9 | constructor (msg) { 10 | super(msg || 'Failed to concatenate video') 11 | this.name = 'VideoConcatError' 12 | } 13 | } 14 | 15 | 16 | 17 | export default class Video { 18 | constructor (opts) { 19 | if (typeof opts.filePaths === 'undefined') throw new Error('Video must be called with opts.filePaths'); 20 | if (typeof opts.cwd === 'undefined') throw new Error('Video must be called with opts.cwd'); 21 | this.filePaths = opts.filePaths 22 | this.cwd = opts.cwd 23 | this.room = opts.room || 'projektmelody' 24 | this.execa = opts.execa || execa 25 | } 26 | 27 | 28 | 29 | getFilesTxt () { 30 | return this.filePaths 31 | .sort((a, b) => a.timestamp - b.timestamp) 32 | .map((d) => `file '${d.file}'`) 33 | .join('\n') 34 | .concat('\n') 35 | } 36 | 37 | 38 | getFilesFile () { 39 | const p = path.join(this.cwd, 'files.txt') 40 | fs.writeFileSync( 41 | p, 42 | this.getFilesTxt(this.filePaths), 43 | { encoding: 'utf-8' } 44 | ) 45 | return p 46 | } 47 | 48 | async concat () { 49 | const target = path.join(this.cwd, `${this.room}-chaturbate-${new Date().valueOf()}.mp4`) 50 | 51 | const { exitCode, killed, stdout, stderr } = await this.execa('ffmpeg', [ 52 | '-y', 53 | '-f', 'concat', 54 | '-safe', '0', 55 | '-i', this.getFilesFile(this.filePaths), 56 | '-c', 'copy', 57 | target 58 | ], { 59 | cwd: this.cwd 60 | }); 61 | 62 | if (exitCode !== 0 || killed !== false) { 63 | throw new VideoConcatError(`exitCode:${exitCode}, killed:${killed}, stdout:${stdout}, stderr:${stderr}`); 64 | } 65 | 66 | return target 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/capture/src/add.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | -------------------------------------------------------------------------------- /packages/capture/src/cb.js: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio' 2 | import fetch from 'node-fetch' 3 | 4 | export async function getRandomRoom () { 5 | const res = await fetch('https://chaturbate.com/') 6 | const body = await res.text() 7 | const $ = cheerio.load(body) 8 | let roomsRaw = $('a[data-room]') 9 | let rooms = [] 10 | $(roomsRaw).each((_, e) => { 11 | rooms.push($(e).attr('href')) 12 | }) 13 | 14 | // greets https://stackoverflow.com/a/4435017/1004931 15 | var randomIndex = Math.floor(Math.random() * rooms.length); 16 | return rooms[randomIndex].replaceAll('/', '') 17 | } -------------------------------------------------------------------------------- /packages/capture/test/Video.test.js: -------------------------------------------------------------------------------- 1 | 2 | import 'dotenv/config' 3 | import Video from '../src/Video.js' 4 | import { dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import path from 'node:path' 7 | import os from 'node:os' 8 | import fs from 'node:fs' 9 | import sinon from 'sinon' 10 | import sinonChai from 'sinon-chai' 11 | import chai, { expect } from 'chai' 12 | 13 | chai.use(sinonChai); 14 | 15 | const __dirname = dirname(fileURLToPath(import.meta.url)); 16 | 17 | 18 | 19 | const dataFixture = [ 20 | { 21 | timestamp: 1, 22 | file: 'mock-stream0.mp4' 23 | }, { 24 | timestamp: 2, 25 | file: 'mock-stream1.mp4' 26 | }, { 27 | timestamp: 3, 28 | file: 'mock-stream2.mp4' 29 | } 30 | ] 31 | 32 | describe('Video', function () { 33 | 34 | let video 35 | 36 | before(() => { 37 | // copy files to /tmp so we dont clutter the fixtures dir 38 | // and simulate cwd being process.env.FUTUREPORN_TMP 39 | dataFixture.forEach((d) => { 40 | fs.copyFileSync( 41 | path.join(__dirname, 'fixtures', d.file), 42 | path.join(os.tmpdir(), d.file) 43 | ) 44 | }) 45 | }) 46 | 47 | 48 | 49 | beforeEach(() => { 50 | video = new Video({ 51 | cwd: os.tmpdir(), 52 | filePaths: dataFixture, 53 | execa: sinon.fake.resolves({ exitCode: 0, killed: false, stdout: "i am so horni rn", stderr: null }) 54 | }) 55 | }) 56 | 57 | afterEach(function() { 58 | console.log('>> sinon.restore! (afterEach)') 59 | sinon.restore(); 60 | }) 61 | 62 | 63 | describe('getFilesTxt', function () { 64 | it('should generate contents suitable for input to `ffmpeg -f concat`', function () { 65 | const txt = video.getFilesTxt() 66 | expect(txt).to.deep.equal("file 'mock-stream0.mp4'\nfile 'mock-stream1.mp4'\nfile 'mock-stream2.mp4'\n") 67 | }) 68 | }) 69 | 70 | describe('concat', function () { 71 | it('should join multiple videos into one', async function () { 72 | const file = await video.concat() 73 | expect(typeof file === 'string').to.be.true 74 | expect(video.execa).calledOnce 75 | expect(file).to.match(/\.mp4$/) 76 | }) 77 | }) 78 | 79 | describe('getFilesFile', function () { 80 | it('should create a files.txt and return the path', async function () { 81 | const file = await video.getFilesFile() 82 | expect(typeof file === 'string').to.be.true 83 | expect(file).to.equal(path.join(os.tmpdir(), 'files.txt')) 84 | }) 85 | }) 86 | }) -------------------------------------------------------------------------------- /packages/capture/test/fixtures/just-a-text-file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/capture/test/fixtures/just-a-text-file.txt -------------------------------------------------------------------------------- /packages/capture/test/fixtures/mock-stream0.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/capture/test/fixtures/mock-stream0.mp4 -------------------------------------------------------------------------------- /packages/capture/test/fixtures/mock-stream1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/capture/test/fixtures/mock-stream1.mp4 -------------------------------------------------------------------------------- /packages/capture/test/fixtures/mock-stream2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/capture/test/fixtures/mock-stream2.mp4 -------------------------------------------------------------------------------- /packages/capture/test/integration/record.test.js: -------------------------------------------------------------------------------- 1 | import { record, assertDependencyDirectory } from '../../src/record.js' 2 | import { getRandomRoom } from '../../src/cb.js' 3 | import path from 'node:path' 4 | import os from 'node:os' 5 | import { execa } from 'execa' 6 | 7 | describe('record', function() { 8 | it('should record a file to disk', async function () { 9 | this.timeout(1000*60) 10 | const roomName = await getRandomRoom() 11 | console.log(`roomName:${roomName}`) 12 | const appContext = { 13 | env: { 14 | FUTUREPORN_WORKDIR: os.tmpdir(), 15 | DOWNLOADER_UA: "Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0" 16 | }, 17 | logger: { 18 | log: (msg) => { console.log(JSON.stringify(msg)) } 19 | } 20 | } 21 | console.log(appContext) 22 | const { stdout } = await execa('yt-dlp', ['-g', `https://chaturbate.com/${roomName}`]) 23 | const playlistUrl = stdout.trim() 24 | console.log(`playlistUrl:${playlistUrl}`) 25 | assertDependencyDirectory(appContext) 26 | const ffmpegProc = record(appContext, playlistUrl, roomName) 27 | // console.log(ffmpegProc) 28 | return new Promise((resolve) => { 29 | setTimeout(() => { 30 | ffmpegProc.kill('SIGINT') 31 | resolve() 32 | }, 1000*10) 33 | }) 34 | }) 35 | }) -------------------------------------------------------------------------------- /packages/chat/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const seedrandom = require('seedrandom'); 3 | const millisecondsToHours = require('date-fns/millisecondsToHours') 4 | 5 | channelName = process.argv[2] || 'projektmelody' 6 | 7 | // greets ChatGPT 8 | function generateRandomString(seed) { 9 | const possibleCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 10 | let random = new seedrandom(seed); 11 | return [...Array(11)].map(i => possibleCharacters[Math.floor(random() * possibleCharacters.length)]).join(''); 12 | } 13 | 14 | 15 | async function getViewerCount(channelName, presenceId = generateRandomString(millisecondsToHours(Date.now()))) { 16 | const res = await fetch(`https://chaturbate.com/push_service/room_user_count/${channelName}/?presence_id=${presenceId}`, { 17 | "credentials": "include", 18 | "headers": { 19 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0", 20 | "Accept": "*/*", 21 | "Accept-Language": "en-US,en;q=0.5", 22 | "Sec-Fetch-Dest": "empty", 23 | "Sec-Fetch-Mode": "no-cors", 24 | "Sec-Fetch-Site": "same-origin", 25 | "Sec-GPC": "1", 26 | "X-Requested-With": "XMLHttpRequest", 27 | "Alt-Used": "chaturbate.com", 28 | "Pragma": "no-cache", 29 | "Cache-Control": "no-cache" 30 | }, 31 | "referrer": `https://chaturbate.com/${channelName}/`, 32 | "method": "GET", 33 | "mode": "cors" 34 | }); 35 | const json = await res.json() 36 | 37 | if (!res.ok) throw new Error(`HTTP request was not OK! STATUS CODE-- ${res.status}`); 38 | return json 39 | } 40 | 41 | 42 | async function main () { 43 | const vc = await getViewerCount(channelName) 44 | console.log(vc) 45 | } 46 | 47 | main() -------------------------------------------------------------------------------- /packages/chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "futureporn-cb-chat", 3 | "dependencies": { 4 | "date-fns": "^2.29.3", 5 | "seedrandom": "^3.0.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/chat/readChat.js: -------------------------------------------------------------------------------- 1 | 2 | const fetch = require('node-fetch'); 3 | 4 | 5 | async function readChat() { 6 | await fetch("wss://chatw-48.stream.highwebmedia.com/ws/150/xvzpfd0m/websocket", { 7 | "credentials": "include", 8 | "headers": { 9 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0", 10 | "Accept": "*/*", 11 | "Accept-Language": "en-US,en;q=0.5", 12 | "Sec-WebSocket-Version": "13", 13 | "Sec-WebSocket-Extensions": "permessage-deflate", 14 | "Sec-WebSocket-Key": "S/zsDSR85q/Fa12zDm5WLA==", 15 | "Sec-Fetch-Dest": "websocket", 16 | "Sec-Fetch-Mode": "websocket", 17 | "Sec-Fetch-Site": "cross-site", 18 | "Sec-GPC": "1", 19 | "Pragma": "no-cache", 20 | "Cache-Control": "no-cache" 21 | }, 22 | "method": "GET", 23 | "mode": "cors" 24 | }); 25 | 26 | } 27 | 28 | 29 | readChat() -------------------------------------------------------------------------------- /packages/chat/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | date-fns@^2.29.3: 6 | version "2.29.3" 7 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" 8 | integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== 9 | 10 | seedrandom@^3.0.5: 11 | version "3.0.5" 12 | resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" 13 | integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== 14 | -------------------------------------------------------------------------------- /packages/commander/.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | 3 | data/ 4 | 5 | -------------------------------------------------------------------------------- /packages/commander/README.md: -------------------------------------------------------------------------------- 1 | # futureporn-commander 2 | 3 | listens to messages and coordinates actions 4 | 5 | ## Database Schema Specification 6 | 7 | ### VOD 8 | 9 | this is what we're familiar with, straight from the .md files 10 | 11 | * title 12 | * videoSrcHash 13 | * date 14 | 15 | etc. 16 | 17 | 18 | ## Pub/Sub Message Specifications 19 | 20 | All messages include 21 | 22 | * sender {String} cuid 23 | * timestamp {Number} ms since epoch 24 | * topic {String} topic relating to the payload. example: "capture/vod/upload" 25 | 26 | 27 | ### Scout 28 | 29 | 30 | #### scout/twitter/link 31 | 32 | > "I see a CB invite tweet. Here is the link." 33 | 34 | 35 | #### scout/stream/start 36 | 37 | > "I see a CB stream that just started." 38 | 39 | 40 | #### scout/stream/stop 41 | 42 | > "I see a CB stream that just stopped." 43 | 44 | 45 | 46 | ### Capture 47 | 48 | Says, "I am starting capture." 49 | Says, "I am stopping capture, here is the metadata." 50 | 51 | ```js 52 | // Stream object 53 | { 54 | room: 'projektmelody', 55 | streamStart: '2023-01-10T06:32:00.000Z', 56 | streamEnd: '2023-01-10T07:00:00.000Z', 57 | fileNames: [ 58 | 'projektmelody 2023-01-09 22_32-projektmelody.mp4', 59 | 'projektmelody 2023-01-09 23_00-projektmelody.mp4' 60 | ], 61 | videoSrcHash: 'bafybeibmpishntx3kv6lckeewixuvrghzclmn47b4723akalhdpjmblhaa' 62 | } 63 | ``` 64 | 65 | #### capture/presense 66 | 67 | > "I just started up. Hello!" 68 | 69 | 70 | #### capture/vod/upload 71 | 72 | > "I uploaded a VOD. here is the CID." 73 | 74 | 75 | #### capture/vod/advertisement 76 | 77 | > "I have a vod (or vod segments.) here is what I got." 78 | 79 | ```js 80 | { 81 | fileNames: [ 82 | 'projektmelody 2023-01-09 22_32-projektmelody.mp4', 83 | 'projektmelody 2023-01-09 23_00-projektmelody.mp4' 84 | ], 85 | workerId: 'zed-zed_cldkg64fa00006ykehpr00y9n', 86 | fileSizes: [ 87 | 4294967296, 88 | 1073741824 89 | ] 90 | } 91 | ``` 92 | 93 | 94 | ### Commander 95 | 96 | #### commander/vod/election 97 | 98 | > "I have elected a specific worker to process/upload their vod segment(s)" 99 | 100 | ```js 101 | { 102 | fileNames: [ 103 | 'projektmelody 2023-01-09 22_32-projektmelody.mp4', 104 | 'projektmelody 2023-01-09 23_00-projektmelody.mp4' 105 | ], 106 | workerId: 'zed-zed_cldkg64fa00006ykehpr00y9n', 107 | fileSizes: [ 108 | 4294967296, 109 | 1073741824 110 | ] 111 | } 112 | ``` 113 | 114 | 115 | ### Builder 116 | 117 | -------------------------------------------------------------------------------- /packages/commander/dev.js: -------------------------------------------------------------------------------- 1 | 2 | import 'dotenv/config' 3 | import { execa } from 'execa' 4 | import postgres from 'postgres' 5 | 6 | if (typeof process.env.POSTGRES_PASSWORD === 'undefined') throw new Error('POSTGRES_PASSWORD not exist in env'); 7 | if (typeof process.env.POSTGRES_USERNAME === 'undefined') throw new Error('POSTGRES_USERNAME not exist in env'); 8 | 9 | const sql = postgres({ 10 | username: process.env.POSTGRES_USERNAME, 11 | password: process.env.POSTGRES_PASSWORD 12 | }) 13 | 14 | function perms () { 15 | console.log('perms') 16 | execa('docker', [ 17 | 'run', 18 | '-it', 19 | '--rm', 20 | '-v', 'pgdata:/var/lib/postgresql/data', 21 | 'bash', 22 | 'chown','-R', '1000:1000', '/var/lib/postgresql/data' 23 | ]).stdout.pipe(process.stdout); 24 | } 25 | 26 | function daemon () { 27 | console.log('postgres') 28 | execa('docker', [ 29 | 'run', 30 | '--rm', 31 | '-p', '5432:5432', 32 | '--name', 'postgres-futureporn', 33 | '-v', 'pgdata:/var/lib/postgresql/data', 34 | '-e', `POSTGRES_USER=${process.env.POSTGRES_USERNAME}`, 35 | '-e', `POSTGRES_PASSWORD=${process.env.POSTGRES_PASSWORD}`, 36 | 'postgres' 37 | ]).stdout.pipe(process.stdout); 38 | } 39 | 40 | async function seed () { 41 | console.log('seeding table') 42 | await sql`CREATE TABLE IF NOT EXISTS vod ( 43 | "id" UUID PRIMARY KEY not null DEFAULT gen_random_uuid() UNIQUE, 44 | "title" TEXT, 45 | "videoSrc" TEXT UNIQUE, 46 | "videoSrcHash" TEXT UNIQUE, 47 | "video720Hash" TEXT UNIQUE, 48 | "video480Hash" TEXT UNIQUE, 49 | "video360Hash" TEXT UNIQUE, 50 | "video240Hash" TEXT UNIQUE, 51 | "thinHash" TEXT UNIQUE, 52 | "thiccHash" TEXT UNIQUE, 53 | "announceTitle" TEXT, 54 | "announceUrl" TEXT, 55 | "date" DATE, 56 | "captureDate" DATE, 57 | "note" TEXT, 58 | "tmpFilePath" TEXT, 59 | "tags" TEXT[] 60 | )` 61 | } 62 | 63 | 64 | 65 | perms() 66 | daemon() 67 | 68 | setTimeout(async () => { 69 | await seed() 70 | console.log('ready') 71 | }, 1000) 72 | -------------------------------------------------------------------------------- /packages/commander/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "futureporn-commander", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "Unlicense", 6 | "private": true, 7 | "type": "module", 8 | "scripts": { 9 | "dev": "concurrently npm:dev:db npm:dev:serve", 10 | "dev:serve": "DEBUG=futureporn/commander nodemon -e \"js mjs njk\" index.js", 11 | "dev:db": "node dev.js", 12 | "start": "node index.js" 13 | }, 14 | "dependencies": { 15 | "@fastify/auth": "^4.2.0", 16 | "@fastify/basic-auth": "^5.0.0", 17 | "@fastify/view": "^7.3.0", 18 | "common": "workspace:*", 19 | "dotenv": "^16.0.3", 20 | "fastify": "^4.11.0", 21 | "nunjucks": "^3.2.3", 22 | "postgres": "^3.3.3" 23 | }, 24 | "devDependencies": { 25 | "concurrently": "^7.6.0", 26 | "execa": "^6.1.0", 27 | "nodemon": "^2.0.20" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/commander/views/command.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Commander 5 | 6 | 12 | 13 | 14 | 15 | 57 | 58 | 59 |
60 |

Command

61 |

orders, pl0x

62 | 63 |

Messages

64 | 65 | 70 | 71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "version": "0.0.1", 4 | "license": "Unlicense", 5 | "private": true, 6 | "dependencies": { 7 | "cuid": "^2.1.8", 8 | "dotenv": "^16.0.3", 9 | "form-data-encoder": "^2.1.4", 10 | "formdata-node": "^5.0.0", 11 | "got": "^12.5.3", 12 | "query-string": "^8.1.0", 13 | "winston": "^3.8.2" 14 | }, 15 | "type": "module", 16 | "exports": { 17 | "./logger": "./src/logger.js", 18 | "./id": "./src/id.js", 19 | "./constants": "./src/constants.js", 20 | "./Cluster": "./src/Cluster.js" 21 | }, 22 | "scripts": { 23 | "test:integration": "mocha ./test/integration" 24 | }, 25 | "devDependencies": { 26 | "chai": "^4.3.7", 27 | "mocha": "^10.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/common/src/add.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import got from 'got' 3 | import https from 'https' 4 | import path from 'node:path' 5 | import { FormData } from 'formdata-node' 6 | import fs, { createWriteStream } from 'node:fs' 7 | import { pipeline } from 'node:stream' 8 | import { promisify } from 'node:util' 9 | import { fileFromPath } from "formdata-node/file-from-path" 10 | 11 | dotenv.config() 12 | 13 | if (typeof process.env.IPFS_CLUSTER_HTTP_API_USERNAME === 'undefined') throw new Error('process.env.IPFS_CLUSTER_HTTP_API_USERNAME undef'); 14 | if (typeof process.env.IPFS_CLUSTER_HTTP_API_PASSWORD === 'undefined') throw new Error('process.env.IPFS_CLUSTER_HTTP_API_PASSWORD undef'); 15 | 16 | 17 | 18 | async function main () { 19 | let cid; 20 | const streamPipeline = promisify(pipeline); 21 | 22 | const agent = new https.Agent({ 23 | rejectUnauthorized: false 24 | }); 25 | 26 | const form = new FormData() 27 | form.set('file', await fileFromPath('/home/chris/Documents/projektmelody/Projekt Melody _ VSHOJO - A.I.s save so much money on closet space-1596526711801319427.mp4')) 28 | 29 | const opts = { 30 | https: { rejectUnauthorized: false }, 31 | body: form, 32 | headers: { 33 | 'Accept': '*/*', 34 | 'Authorization': `Basic ${Buffer.from(process.env.IPFS_CLUSTER_HTTP_API_USERNAME+':'+process.env.IPFS_CLUSTER_HTTP_API_PASSWORD).toString('base64')}` 35 | }, 36 | } 37 | const res = await got.stream.post('https://sbtp.xyz:9094/add?cid-version=1&progress=1', opts) 38 | 39 | res.on('data', (data) => { 40 | if (typeof data?.cid !== 'undefined') { 41 | resolve(data.cid) 42 | } 43 | }) 44 | 45 | res.on('error', (error) => { 46 | reject(error) 47 | }) 48 | } 49 | 50 | 51 | main().then((cid) => { 52 | console.log(`CID:${cid}`) 53 | }).catch((error) => { 54 | console.error(error) 55 | }) -------------------------------------------------------------------------------- /packages/common/src/constants.js: -------------------------------------------------------------------------------- 1 | export const ipfsHashRegex = /Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,}/; 2 | -------------------------------------------------------------------------------- /packages/common/src/dump.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: application/json 5 | Trailer: X-Stream-Error 6 | Vary: Origin 7 | X-Chunked-Output: 1 8 | Date: Tue, 21 Feb 2023 20:27:04 GMT 9 | Transfer-Encoding: chunked 10 | 11 | -------------------------------------------------------------------------------- /packages/common/src/dump_bak.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Cache-Control: no-cache 3 | Connection: close 4 | Content-Type: application/json 5 | Trailer: X-Stream-Error 6 | Vary: Origin 7 | X-Chunked-Output: 1 8 | Date: Tue, 21 Feb 2023 20:21:01 GMT 9 | Transfer-Encoding: chunked 10 | 11 | -------------------------------------------------------------------------------- /packages/common/src/id.js: -------------------------------------------------------------------------------- 1 | 2 | import os from 'os' 3 | import cuid from 'cuid' 4 | 5 | export const workerId = `${os.hostname}-${cuid()}` 6 | -------------------------------------------------------------------------------- /packages/common/src/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | 3 | export const loggerFactory = (options) => { 4 | const mergedOptions = Object.assign({}, { 5 | level: 'info', 6 | defaultMeta: { service: 'futureporn' } 7 | }, options) 8 | const logger = winston.createLogger(mergedOptions); 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | logger.add(new winston.transports.Console({ 12 | level: 'debug', 13 | format: winston.format.simple() 14 | })) 15 | } else { 16 | logger.add(new winston.transports.Console({ 17 | level: 'info', 18 | format: winston.format.json() 19 | })) 20 | } 21 | 22 | return logger 23 | } 24 | 25 | -------------------------------------------------------------------------------- /packages/common/src/res.json: -------------------------------------------------------------------------------- 1 | {"name":"","cid":"bafkreifsrsklegk4r3jft4fucwvo4pzzwczjecsfg5qrjgp2arevnel2ee","size":15,"allocations":["QmUdgoP4kxH6QS38zs13655PhYyn1vVK9atv6HJReC2rKL","QmUwRKEps2dWU1djy1iiWHkw5LfyCoiFFBt3BigMW9zEQ5","QmZSZJeo3o3q2q3M5VTtH6UrgWEodYLmhRwpaoU8uQzbrM"]} 2 | -------------------------------------------------------------------------------- /packages/common/test/fixtures/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/common/test/fixtures/screenshot.png -------------------------------------------------------------------------------- /packages/common/test/integration/ipfsCluster.test.js: -------------------------------------------------------------------------------- 1 | import Cluster from '../../src/Cluster.js' 2 | import path, { dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { expect } from 'chai'; 5 | import dotenv from 'dotenv' 6 | dotenv.config() 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | 9 | 10 | const screenshotFixtureCid = 'bafybeidr7sbjslkexppjfos2irsyghgvghjgvhf5fessetmha7dt4pcjmu' 11 | const screenshotFixturePath = path.join(__dirname, '..', 'fixtures', 'screenshot.png') 12 | 13 | describe('Cluster', function () { 14 | describe('add', function () { 15 | it('should upload a file', async function () { 16 | this.timeout(1000*30) 17 | const cluster = new Cluster({ 18 | username: process.env.IPFS_CLUSTER_HTTP_API_USERNAME, 19 | password: process.env.IPFS_CLUSTER_HTTP_API_PASSWORD, 20 | uri: 'https://sbtp.xyz:9094' 21 | }) 22 | const res = await cluster.add(screenshotFixturePath) 23 | expect(res).to.have.property('cid', screenshotFixtureCid) 24 | }) 25 | it('should upload a bigger file', async function () { 26 | this.timeout(1000*60*3) 27 | const cluster = new Cluster({ 28 | username: process.env.IPFS_CLUSTER_HTTP_API_USERNAME, 29 | password: process.env.IPFS_CLUSTER_HTTP_API_PASSWORD, 30 | uri: 'https://sbtp.xyz:9094' 31 | }) 32 | const res = await cluster.add('/home/chris/Documents/projektmelody/Projekt Melody _ VSHOJO - A.I.s save so much money on closet space-1596526711801319427.mp4') 33 | expect(res).to.have.property('cid') 34 | }) 35 | }) 36 | }) -------------------------------------------------------------------------------- /packages/render/node_modules/common: -------------------------------------------------------------------------------- 1 | ../../common -------------------------------------------------------------------------------- /packages/render/node_modules/dotenv: -------------------------------------------------------------------------------- 1 | ../../../node_modules/.pnpm/dotenv@16.0.3/node_modules/dotenv -------------------------------------------------------------------------------- /packages/render/node_modules/fastq: -------------------------------------------------------------------------------- 1 | ../../../node_modules/.pnpm/fastq@1.15.0/node_modules/fastq -------------------------------------------------------------------------------- /packages/render/node_modules/postgres: -------------------------------------------------------------------------------- 1 | ../../../node_modules/.pnpm/postgres@3.3.3/node_modules/postgres -------------------------------------------------------------------------------- /packages/render/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "render", 3 | "version": "0.0.1", 4 | "description": "Futureporn component which transcodes video different formats and generates thumbnails", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index" 9 | }, 10 | "keywords": [], 11 | "author": "CJ_Clippy ", 12 | "license": "Unlicense", 13 | "type": "module", 14 | "dependencies": { 15 | "@paralleldrive/cuid2": "^2.2.0", 16 | "common": "workspace:^0.0.1", 17 | "dotenv": "^16.0.3", 18 | "execa": "^6.1.0", 19 | "fastq": "^1.15.0", 20 | "got": "^12.5.3", 21 | "magic-bytes.js": "^1.0.14", 22 | "node-tar": "^1.0.0", 23 | "postgres": "^3.3.3", 24 | "prevvy": "^5.0.1", 25 | "tar": "^6.1.13", 26 | "tar-fs": "^2.1.1", 27 | "tar-stream": "^3.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/render/test/render.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/packages/render/test/render.test.js -------------------------------------------------------------------------------- /packages/scout/.nvmrc: -------------------------------------------------------------------------------- 1 | lts/hydrogen 2 | -------------------------------------------------------------------------------- /packages/scout/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // import twitter from './src/twitter.js' 4 | import 'dotenv/config' 5 | // import { chat, getViewerCount, monitorRealtimeStatus } from './src/chaturbate.js' 6 | import Room from './src/Room.js' 7 | import { containsCBInviteLink } from "./src/tweetProcess.js" 8 | 9 | import { loggerFactory } from "common/logger" 10 | 11 | 12 | import postgres from 'postgres' 13 | 14 | if (typeof process.env.POSTGRES_HOST === 'undefined') throw new Error('POSTGRES_HOST undef'); 15 | if (typeof process.env.POSTGRES_USERNAME === 'undefined') throw new Error('POSTGRES_USERNAME undef'); 16 | if (typeof process.env.POSTGRES_PASSWORD === 'undefined') throw new Error('POSTGRES_PASSWORD undef'); 17 | 18 | 19 | const logger = loggerFactory({ 20 | defaultMeta: { service: 'futureporn/scout' } 21 | }) 22 | 23 | const sql = postgres({ 24 | user: process.env.POSTGRES_USERNAME, 25 | password: process.env.POSTGRES_PASSWORD, 26 | host: process.env.POSTGRES_HOST 27 | }) 28 | 29 | 30 | /** 31 | * tweetConsumer 32 | * 33 | * this is the function that is called when a tweet is detected 34 | */ 35 | const tweetConsumer = (tweet) => { 36 | logger.log({ level: 'debug', message: ` [*] Tweet: ${JSON.stringify(tweet, 0, 2)}` }) 37 | 38 | if (containsCBInviteLink(tweet)) { 39 | logger.log({ level: 'debug', message: ` [*] The tweet contains a CB invite link.` }) 40 | aedes.publish('futureporn/scout/tweet', tweet) 41 | } 42 | } 43 | 44 | 45 | const onCbStart = () => { 46 | sql.notify('scout/stream/start', { date: new Date().valueOf() }) 47 | } 48 | 49 | 50 | const onCbStop = () => { 51 | sql.notify('scout/stream/stop', { date: new Date().valueOf() }) 52 | } 53 | 54 | 55 | /** 56 | * main 57 | * 58 | * - connect to twitter and listen for new tweets 59 | * - connect to chaturbate chat and watch for spikes in messages per minute 60 | */ 61 | async function main () { 62 | // twitter(tweetConsumer) 63 | // monitorRealtimeStatus('projektmelody', onCbStart, onCbStop) 64 | const room = new Room({ 65 | roomName: 'projektmelody', 66 | onStart: onCbStart, 67 | onStop: onCbStop 68 | }) 69 | room.monitorRealtime() 70 | } 71 | 72 | logger.log({ level: 'info', message: 'hello' }) 73 | logger.log({ level: 'info', message: `process.env.NODE_ENV:${process.env.NODE_ENV}` }) 74 | 75 | main() 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /packages/scout/index.js.old: -------------------------------------------------------------------------------- 1 | import common from 'common' 2 | 3 | common() -------------------------------------------------------------------------------- /packages/scout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scout", 3 | "version": "0.0.1", 4 | "description": "event emitter that detects start and end of stream", 5 | "main": "index.js", 6 | "license": "Unlicense", 7 | "private": true, 8 | "scripts": { 9 | "start": "node index", 10 | "test": "mocha ./test/unit", 11 | "dev": "FUTUREPORN_WORKDIR=/home/chris/Downloads yarn nodemon index" 12 | }, 13 | "type": "module", 14 | "dependencies": { 15 | "ably": "1.2.13", 16 | "cheerio": "^1.0.0-rc.12", 17 | "common": "workspace:^0.0.1", 18 | "date-fns": "^2.29.3", 19 | "dotenv": "^16.0.3", 20 | "fetch-blob": "^3.2.0", 21 | "formdata-polyfill": "^4.0.10", 22 | "node-fetch": "^3.3.0", 23 | "postgres": "^3.3.3", 24 | "seedrandom": "^3.0.5", 25 | "tough-cookie": "^4.1.2", 26 | "tough-cookie-file-store": "^2.0.3", 27 | "twitter-v2": "^1.1.0", 28 | "winston": "^3.8.2" 29 | }, 30 | "devDependencies": { 31 | "chai": "^4.3.7", 32 | "chai-events": "^0.0.3", 33 | "mocha": "^10.2.0", 34 | "nodemon": "^2.0.20" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/scout/src/constants.js: -------------------------------------------------------------------------------- 1 | export const projektMelodyTwitterId = '1148121315943075841' 2 | export const projektMelodyCbRoomId = 'G0TWFS5' -------------------------------------------------------------------------------- /packages/scout/taco.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JS Bin 7 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/scout/test/integration/Room.test.js: -------------------------------------------------------------------------------- 1 | 2 | import chai, { expect } from "chai"; 3 | import { EventEmitter } from "node:events"; 4 | import chaiEvents from 'chai-events' 5 | import Room from '../../src/Room.js' 6 | 7 | import { projektMelodyCbRoomId } from '../../src/constants.js' 8 | 9 | chai.use(chaiEvents); 10 | 11 | 12 | describe('Room', function () { 13 | beforeEach(function (done) { 14 | // courtesy delay 15 | setTimeout(function(){ 16 | done(); 17 | }, 1000); 18 | }); 19 | it('should get a room id', async function () { 20 | const room = new Room({ 21 | roomName: 'projektmelody' 22 | }) 23 | const roomId = await room.getRoomId() 24 | expect(roomId).to.equal(projektMelodyCbRoomId) 25 | expect(room).to.have.property('roomId', projektMelodyCbRoomId) 26 | }) 27 | 28 | describe('getCsrfToken', function () { 29 | it('should get a token from CB', async function () { 30 | const room = new Room({ 31 | roomName: 'projektmelody' 32 | }) 33 | const token = await room.getCsrfToken() 34 | expect(typeof(token) === 'string').to.be.true 35 | expect(token.length).to.equal(64) 36 | }) 37 | }) 38 | 39 | describe('getPushServiceAuth', function () { 40 | it('should resolve with a valid TokenRequest', async function () { 41 | const room = new Room({ 42 | roomName: 'projektmelody' 43 | }) 44 | const tokenRequest = await room.getPushServiceAuth() 45 | expect(tokenRequest).to.have.property('token_request') 46 | expect(tokenRequest.token_request).to.have.property('timestamp') 47 | expect(tokenRequest.token_request).to.have.property('ttl') 48 | }) 49 | it('should re-use the token as long as its not expired', async function () { 50 | const room = new Room({ 51 | roomName: 'projektmelody' 52 | }) 53 | const tokenRequest1 = await room.getPushServiceAuth() 54 | const tokenRequest2 = await room.getPushServiceAuth() 55 | expect(tokenRequest1.token).to.equal(tokenRequest2.token) 56 | }) 57 | }) 58 | 59 | }) -------------------------------------------------------------------------------- /packages/scout/testdex.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // import twitter from './src/twitter.js' 4 | import 'dotenv/config' 5 | // import { chat, getViewerCount, monitorRealtimeStatus } from './src/chaturbate.js' 6 | import Room from './src/Room.js' 7 | import { containsCBInviteLink } from "./src/tweetProcess.js" 8 | 9 | import { loggerFactory } from "common/logger" 10 | 11 | 12 | import postgres from 'postgres' 13 | 14 | if (typeof process.env.POSTGRES_HOST === 'undefined') throw new Error('POSTGRES_HOST undef'); 15 | if (typeof process.env.POSTGRES_USERNAME === 'undefined') throw new Error('POSTGRES_USERNAME undef'); 16 | if (typeof process.env.POSTGRES_PASSWORD === 'undefined') throw new Error('POSTGRES_PASSWORD undef'); 17 | 18 | 19 | const logger = loggerFactory({ 20 | defaultMeta: { service: 'futureporn/scout' } 21 | }) 22 | 23 | const sql = postgres({ 24 | user: process.env.POSTGRES_USERNAME, 25 | password: process.env.POSTGRES_PASSWORD, 26 | host: process.env.POSTGRES_HOST 27 | }) 28 | 29 | 30 | /** 31 | * tweetConsumer 32 | * 33 | * this is the function that is called when a tweet is detected 34 | */ 35 | const tweetConsumer = (tweet) => { 36 | logger.log({ level: 'debug', message: ` [*] Tweet: ${JSON.stringify(tweet, 0, 2)}` }) 37 | 38 | if (containsCBInviteLink(tweet)) { 39 | logger.log({ level: 'debug', message: ` [*] The tweet contains a CB invite link.` }) 40 | aedes.publish('futureporn/scout/tweet', tweet) 41 | } 42 | } 43 | 44 | 45 | const onCbStart = () => { 46 | sql.notify('scout/stream/start', { date: new Date().valueOf() }) 47 | } 48 | 49 | 50 | const onCbStop = () => { 51 | sql.notify('scout/stream/stop', { date: new Date().valueOf() }) 52 | } 53 | 54 | 55 | /** 56 | * main 57 | * 58 | * - connect to twitter and listen for new tweets 59 | * - connect to chaturbate chat and watch for spikes in messages per minute 60 | */ 61 | async function main () { 62 | // twitter(tweetConsumer) 63 | // monitorRealtimeStatus('projektmelody', onCbStart, onCbStop) 64 | const room = new Room({ 65 | roomName: 'kronniekray', 66 | onStart: onCbStart, 67 | onStop: onCbStop 68 | }) 69 | room.monitorRealtime() 70 | } 71 | 72 | logger.log({ level: 'info', message: 'hello' }) 73 | logger.log({ level: 'info', message: `process.env.NODE_ENV:${process.env.NODE_ENV}` }) 74 | 75 | main() 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /packages/vibe/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import roboflow from '../src/roboflow.js' 4 | 5 | 6 | async function sampleImageUpload () { 7 | const vod = await getRandomVod() 8 | debug(vod) 9 | const cid = vod[0].videoSrcHash 10 | debug(` [*] randomVodCid: ${cid}`) 11 | const url = `https://ipfs.io/ipfs/${cid}` 12 | debug(` [*] url:${url}`) 13 | 14 | 15 | const videoDuration = await VideoLength(url, { 16 | bin: '/usr/bin/mediainfo' 17 | }) 18 | debug(` [*] videoDuration:${videoDuration}`) 19 | const randomTime = randomFloatBetween(60*5, videoDuration) 20 | debug(` [*] randomTime:${randomTime}`) 21 | const frameFilename = await ffmpegGrab(url, randomTime) 22 | debug(` [*] frameFilename:${frameFilename}`) 23 | const res = await upload(frameFilename) 24 | debug(res) 25 | } 26 | 27 | 28 | async function main () { 29 | 30 | } -------------------------------------------------------------------------------- /packages/vibe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Chris Grimmett ", 6 | "license": "Unlicense", 7 | "private": true, 8 | "devDependencies": { 9 | "mocha": "^10.2.0" 10 | }, 11 | "dependencies": { 12 | "debug": "^4.3.4", 13 | "dotenv": "^16.0.3", 14 | "execa": "^6.1.0", 15 | "fluent-ffmpeg": "^2.1.2", 16 | "formdata-node": "^5.0.0", 17 | "node-fetch": "^3.3.0", 18 | "postgres": "^3.3.3", 19 | "video-length": "^2.0.6" 20 | }, 21 | "type": "module", 22 | "scripts": { 23 | "test": "mocha" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/vibe/roboflow/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import roboflow from '../src/roboflow.js' 4 | 5 | 6 | async function sampleImageUpload () { 7 | const vod = await getRandomVod() 8 | debug(vod) 9 | const cid = vod[0].videoSrcHash 10 | debug(` [*] randomVodCid: ${cid}`) 11 | const url = `https://ipfs.io/ipfs/${cid}` 12 | debug(` [*] url:${url}`) 13 | 14 | 15 | const videoDuration = await VideoLength(url, { 16 | bin: '/usr/bin/mediainfo' 17 | }) 18 | debug(` [*] videoDuration:${videoDuration}`) 19 | const randomTime = randomFloatBetween(60*5, videoDuration) 20 | debug(` [*] randomTime:${randomTime}`) 21 | const frameFilename = await ffmpegGrab(url, randomTime) 22 | debug(` [*] frameFilename:${frameFilename}`) 23 | const res = await upload(frameFilename) 24 | debug(res) 25 | } 26 | 27 | 28 | async function main () { 29 | 30 | } -------------------------------------------------------------------------------- /packages/vibe/src/roboflow.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import fetch from "node-fetch"; 3 | import path from "node:path" 4 | import {FormData} from 'formdata-node' 5 | import {fileFromPath} from "formdata-node/file-from-path" 6 | 7 | 8 | if (typeof process.env.ROBOFLOW_API_KEY === 'undefined') 9 | throw new Error('ROBOFLOW_API_KEY is undefined'); 10 | 11 | const datasetName = 'lovense-levels' 12 | 13 | 14 | export async function upload (filename) { 15 | const formData = new FormData(); 16 | formData.set("name", filename); 17 | formData.set("api_key", process.env.ROBOFLOW_API_KEY) 18 | formData.set("file", await fileFromPath(filename)); 19 | formData.set("split", "train"); 20 | const url = `https://api.roboflow.com/dataset/${datasetName}/upload` 21 | const res = await fetch(url, { 22 | headers: { 23 | 'Authorization': `Bearer ${process.env.ROBOFLOW_API_KEY}` 24 | }, 25 | method: "POST", 26 | body: formData 27 | }) 28 | const json = await res.json() 29 | console.log(json) 30 | return json 31 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /test/cid.test.js: -------------------------------------------------------------------------------- 1 | 2 | const chai = require('chai'); 3 | 4 | const { convertToV1, isCidV0 } = require('../utils/cid.js') 5 | 6 | 7 | const cidV0Fixture = "QmTYN7LXsTU3WV2cUndqK32Bd98TEiGqV2UgDQAfmYsU72" 8 | const cidV1Fixture = "bafybeicnjkrkbkhfyllhvoo5vx2wwxbld2ecqqjhv22o7lv3zob5jgulne" 9 | const cidV1FixtureWithPath = "bafybeicnjkrkbkhfyllhvoo5vx2wwxbld2ecqqjhv22o7lv3zob5jgulne?filename=20201011T220530Z-thicc.jpg" 10 | const cidV0FixtureWithPath = "QmTYN7LXsTU3WV2cUndqK32Bd98TEiGqV2UgDQAfmYsU72?filename=20201011T220530Z-thicc.jpg" 11 | 12 | describe('convertToV1', function () { 13 | it('should convert a v0 to v1', async function() { 14 | const v1 = await convertToV1(cidV0Fixture) 15 | chai.expect(v1).to.equal(cidV1Fixture) 16 | }) 17 | it('should handle a cid with a querystring', async function () { 18 | const v1 = await convertToV1(cidV0FixtureWithPath) 19 | chai.expect(v1).to.equal(cidV1FixtureWithPath) 20 | }) 21 | }) 22 | 23 | 24 | describe('isCidV0', function() { 25 | it('should return true when the CID is v0', function () { 26 | chai.expect(isCidV0(cidV0Fixture)).to.be.true 27 | }) 28 | it('should handle a v0 CID with querystring', function () { 29 | chai.expect(isCidV0(cidV0FixtureWithPath)).to.be.true 30 | }) 31 | it('should return false when teh CID is v1', function () { 32 | chai.expect(isCidV0(cidV1Fixture)).to.be.false 33 | }) 34 | it('should handle a v1 CID with querystring', function () { 35 | chai.expect(isCidV0(cidV1FixtureWithPath)).to.be.false 36 | }) 37 | }) -------------------------------------------------------------------------------- /test/cj_clippy_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/test/cj_clippy_avatar.png -------------------------------------------------------------------------------- /test/ipfsClusterUpload.test.js: -------------------------------------------------------------------------------- 1 | const ipfsClusterUpload = require('../utils/ipfsClusterUpload.js'); 2 | const path = require('node:path'); 3 | 4 | describe('ipfsClusterUpload', () => { 5 | it('integration', async () => { 6 | this.timeout(3*60*1000) 7 | 8 | const testAvatarPath = path.join(__dirname, 'cj_clippy_avatar.png'); 9 | const res = await ipfsClusterUpload(testAvatarPath); 10 | 11 | expect(res).toBe('QmZdHWWxQgZdBux8tMDdZdDTqgbumEcHgZWusN69Ko5A3T'); 12 | }) 13 | }) -------------------------------------------------------------------------------- /test/testvid.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/test/testvid.mkv -------------------------------------------------------------------------------- /test/testvid.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/test/testvid.mp4 -------------------------------------------------------------------------------- /test/twitchy-gimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insanity54/futureporn/21f4946ea8ab1fc722e2dfde762155784c24fb6e/test/twitchy-gimp.png -------------------------------------------------------------------------------- /utils/checklist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # checklist.sh 4 | # 5 | # Futureporn checklist 6 | # This is a checklist to help me ensure that I do everything necessary 7 | # after a new vod is created. 8 | # This idea borrows from 9 | # [Do-nothing scripting: the key to gradual automation](https://blog.danslimmon.com/2019/07/15/do-nothing-scripting-the-key-to-gradual-automation/) 10 | # [HN Thread](https://news.ycombinator.com/item?id=29083367) 11 | 12 | 13 | echo "Futureporn checklist.sh" 14 | echo "This is a process checklist of tasks that must be completed after a new VOD is created." 15 | echo "Press [Enter] to complete a task." 16 | echo "" 17 | 18 | echo "[TASK] Open Portal" 19 | echo " ex: xdg-open https://portal.futureporn.net/admin" 20 | read 21 | 22 | echo "[TASK] add date, announceUrl, announceTitle, title to Portal" 23 | read 24 | 25 | echo "[TASK] add the file to ipfs" 26 | echo " ex: ipfs add --cid-version=1 ./file.mp4" 27 | read 28 | 29 | echo "[TASK] add videoSrcHash to Portal" 30 | read 31 | 32 | echo "[TASK] backup the file to B2" 33 | echo " ex: b2-linux upload-file futureporn ./file.mp4 file.mp4" 34 | read 35 | 36 | echo "[TASK] add B2 URL to Portal (videoSrc)" 37 | read 38 | 39 | echo "[TASK] publish VOD on Portal" 40 | read 41 | 42 | echo "[TASK] Trigger a website build" 43 | echo " ex: https://app.fleek.co/#/sites/futureporn/deploys?accountId=insanity54-team" 44 | read 45 | 46 | echo "[TASK] Take a break" 47 | echo " ex: walk" 48 | echo " ex: sleep" 49 | echo " ex: eat" 50 | read 51 | -------------------------------------------------------------------------------- /utils/cid.js: -------------------------------------------------------------------------------- 1 | 2 | const execa = require('execa') 3 | const { cidV0Regex } = require('./constants.js') 4 | 5 | 6 | 7 | async function convertToV1 (val) { 8 | if (typeof val === 'undefined') throw new Error('convertToV1 received an undefined argument'); 9 | let cid; 10 | let v1Cid; 11 | let p; 12 | if (val.includes('?')) { 13 | p = val.split('?') 14 | cid = p[0] 15 | } else { 16 | cid = val 17 | } 18 | 19 | const { exitCode, killed, stdout, stderr } = await execa('ipfs', ['cid', 'base32', cid]); 20 | if (exitCode !== 0 || killed !== false) { 21 | throw new Error(`exitCode:${exitCode}, killed:${killed}, stdout:${stdout}, stderr:${stderr}`); 22 | } else { 23 | if (typeof p !== 'undefined' && p.length > 0) { 24 | v1Cid = `${stdout}?${p[1]}` 25 | } else { 26 | v1Cid = stdout 27 | } 28 | } 29 | return v1Cid 30 | } 31 | 32 | function isCidV0 (txt) { 33 | return cidV0Regex.test(txt) 34 | } 35 | 36 | 37 | module.exports = { 38 | convertToV1, 39 | isCidV0 40 | } -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | localTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, 3 | projektMelodyEpoch: new Date('2020-02-07T23:21:48.000Z'), 4 | later: function later(delay, value) { 5 | // greets stackoverflow, i think 6 | return new Promise(resolve => setTimeout(resolve, delay, value)); 7 | }, 8 | cidV0Regex: /Qm[a-zA-Z0-9]{44}/ 9 | } -------------------------------------------------------------------------------- /utils/export.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | const path = require('path'); 5 | const globby = require('globby'); 6 | const matter = require('gray-matter'); 7 | const fsp = require('fs').promises; 8 | const execa = require('execa'); 9 | const { format } = require('date-fns'); 10 | const minimist = require('minimist'); 11 | const { __, curry, not, or, isEmpty, isNil, append, filter, flatten, props, pluck, compose, transduce } = require('ramda'); 12 | 13 | 14 | 15 | /** ansible uses this function to generate a list of IPFS hashes that the gateway will serve **/ 16 | const getVodsAsJson = async () => { 17 | let list = []; 18 | const vods = await globby(path.join(__dirname, '../website/vods/*.md')); 19 | for (vod of vods) { 20 | const content = await fsp.readFile(vod, { encoding: 'utf-8' }); 21 | const data = await matter(content) 22 | list.push(data) 23 | } 24 | return list 25 | } 26 | 27 | 28 | 29 | 30 | 31 | 32 | (async function main () { 33 | 34 | const args = minimist(process.argv.slice(2)) 35 | if (!args.mode) { 36 | console.error('--mode is required. One of (cidlist|all)') 37 | return 38 | } 39 | 40 | 41 | const md = await getVodsAsJson(); 42 | const vods = pluck('data', md) 43 | if (args.mode === 'cidlist') { 44 | 45 | const getProps = props ([ 46 | 'videoSrcHash', 47 | 'video720Hash', 48 | 'video480Hash', 49 | 'video360Hash', 50 | 'video240Hash', 51 | 'thinHash', 52 | 'thiccHash' 53 | ]) 54 | 55 | 56 | const cidlist = vods 57 | .flatMap(getProps) 58 | .filter((i) => not(isNil(i))) 59 | .filter((i) => not(isEmpty(i))) 60 | .map((h) => { 61 | const index = h.indexOf('?'); 62 | if (index > -1) return h.substring(0, h.indexOf('?')) 63 | else return h 64 | }) 65 | 66 | 67 | console.log(JSON.stringify(cidlist)); 68 | } else if (args.mode === 'all') { 69 | console.log(JSON.stringify(vods)); 70 | } 71 | })() 72 | 73 | 74 | module.exports = { 75 | getVodsAsJson 76 | } -------------------------------------------------------------------------------- /utils/getDateFromTwitter.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const debug = require('debug')('futureporn') 4 | const argv = require('argv'); 5 | const VOD = require('./VOD.js'); 6 | const options = { 7 | name: 'announceUrl', 8 | short: 'u', 9 | type: 'string', 10 | description: 'The URL to a tweet', 11 | }; 12 | const { later, localTimeZone } = require('./constants'); 13 | const { utcToZonedTime, format } = require('date-fns-tz'); 14 | const args = argv.option( options ).run(); 15 | 16 | (async () => { 17 | if (typeof args.options.announceUrl === 'undefined') throw new Error('--announceUrl was undefined but it must be defined.'); 18 | 19 | const v = new VOD({ 20 | announceUrl: args.options.announceUrl 21 | }); 22 | 23 | const vv = await v.getDateFromTwitter(); 24 | 25 | debug(' [d]date is as follows'); 26 | debug(` [d] ${vv.date}`); 27 | debug(` [d] getDatestamp result: ${v.getDatestamp()}`); 28 | 29 | const output = { 30 | safeDatestamp: v.getSafeDatestamp(), 31 | datestamp: v.getDatestamp() 32 | } 33 | 34 | console.log(output) 35 | 36 | })(); 37 | -------------------------------------------------------------------------------- /utils/goog-dl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Download a large file from Google using the CLI 4 | # greets https://medium.com/@acpanjan/download-google-drive-files-using-wget-3c2c025a8b99 5 | 6 | 7 | FILEID="${1}" 8 | FILENAME="${2}" 9 | 10 | 11 | if [ -z ${FILEID} ]; then 12 | ekko "First param must be a file ID, but the first param was empty." 13 | exit 6 14 | fi 15 | 16 | if [ -z ${FILENAME} ]; then 17 | ekko "Secpmd param must be a filename, but the first param was empty." 18 | exit 7 19 | fi 20 | 21 | wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate "https://docs.google.com/uc?export=download&id=$FILEID" -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=$FILEID" -O $FILENAME && rm -rf /tmp/cookies.txt 22 | 23 | 24 | -------------------------------------------------------------------------------- /utils/phdl: -------------------------------------------------------------------------------- 1 | q 2 | #!/bin/bash 3 | 4 | 5 | 6 | ## CONFIG 7 | init_delay=10 # The starting retry delay (in seconds.) 8 | max_delay=$((60*10)) # The maximum amount of time to delay beteween checking for a live stream. 9 | delay="${init_delay}" # Ongoing delay counter. Doubles itself if there is no live stream. 10 | 11 | 12 | 13 | 14 | ## VARIABLES 15 | url="${1}" 16 | 17 | ## FUNCTIONS 18 | ekko () { 19 | echo "[$(date)] - ${1}" 20 | } 21 | 22 | init () { 23 | 24 | if [ -z ${url} ]; then 25 | ekko "First param must be a URL, but the first param was empty." 26 | exit 6 27 | fi 28 | 29 | ekko "Attempting to download video at URL ${url}... Press Ctrl+C to quit." 30 | } 31 | 32 | 33 | main () { 34 | while :; do 35 | 36 | # Attempt to download the video. 37 | # We use the name parameter sent to this script to look up the stream url in `ref.json`. 38 | youtube-dl --abort-on-unavailable-fragment --limit-rate 1M "${url}" 39 | 40 | # we are done if youtube-dl exited with 0 error code 41 | if [ $? -eq 0 ]; then 42 | ekko "omg teh exit code was zero, yay!" 43 | exit 0 44 | fi 45 | 46 | # Slowly increase the delay time between retries. 47 | # This is done to be polite to the streaming platform. 48 | # We wait longer and longer between tries, eventually maxing out at ${max_delay} seconds 49 | if [ $(($delay*2)) -ge $max_delay ]; 50 | then delay=$max_delay 51 | else delay=$(($delay*2)); 52 | fi 53 | 54 | 55 | ekko "Retrying in ${delay} seconds..." 56 | sleep "${delay}" 57 | 58 | done 59 | } 60 | 61 | 62 | init 63 | main 64 | -------------------------------------------------------------------------------- /utils/put-files-from-fs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const dotenv = require('dotenv'); 4 | const minimist = require('minimist'); 5 | const { Web3Storage, getFilesFromPath } = require('web3.storage'); 6 | const debug = require('debug')('futureporn'); 7 | dotenv.config(); 8 | 9 | async function upload (storage, files, attempt = 0) { 10 | try { 11 | attempt++; 12 | 13 | debug(`uploading ${files}`); 14 | const rootCid = await storage.put(files) 15 | 16 | console.log(`the rootCid is ${rootCid}`); 17 | 18 | const res = await storage.get(rootCid); // Promise 19 | const ipfsFiles = await res.files(); // Promise 20 | 21 | const cid = ipfsFiles[0].cid; 22 | debug(`the file cid is ${cid}`) 23 | 24 | return cid; 25 | 26 | } catch (e) { 27 | console.error(e); 28 | debug(`upload error at attempt ${attempt}. trying again.`); 29 | 30 | if (attempt > 2) throw new Error(`Upload failed. Tried ${attempt} times.`); 31 | else return upload(storage, files, attempt); 32 | } 33 | } 34 | 35 | 36 | async function main () { 37 | const args = minimist(process.argv.slice(2)) 38 | 39 | const token = process.env.WEB3_TOKEN; 40 | if (typeof token === 'undefined') { 41 | return console.error('A token is needed. (WEB3_TOKEN in env must be defined). You can create one on https://web3.storage. ') 42 | } 43 | 44 | const fileList = args._; 45 | debug('creating storage instance') 46 | const storage = new Web3Storage({ token }) 47 | 48 | debug('getting files from path') 49 | const files = await getFilesFromPath(fileList) 50 | debug(files) 51 | 52 | const cid = await upload(storage, files); 53 | 54 | const result = { 55 | cid 56 | } 57 | 58 | console.log(JSON.stringify(result)); 59 | } 60 | 61 | main() 62 | -------------------------------------------------------------------------------- /utils/roboflow.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import fetch from "node-fetch"; 3 | import fs from "fs"; 4 | import FormData from "form-data"; 5 | 6 | if (typeof process.env.ROBOFLOW_API_KEY === 'undefined') 7 | throw new Error('ROBOFLOW_API_KEY is undefined'); 8 | 9 | const datasetName = 'lovense-levels' 10 | 11 | 12 | export function upload (filename) { 13 | const formData = new FormData(); 14 | formData.append("name", filename); 15 | formData.append("file", fs.createReadStream(filename)); 16 | formData.append("split", "train"); 17 | 18 | fetch({ 19 | method: "POST", 20 | url: `https://api.roboflow.com/dataset/${datasetName}/upload`, 21 | params: { 22 | api_key: process.env.ROBOFLOW_API_KEY 23 | }, 24 | data: formData, 25 | headers: formData.getHeaders() 26 | }) 27 | .then(function (response) { 28 | console.log(response.data); 29 | }) 30 | .catch(function (error) { 31 | console.log(error.message); 32 | }); 33 | } -------------------------------------------------------------------------------- /utils/tweetBlacklist.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These twitter tweets do not contain Chaturbate invite links 3 | * even though they have a chaturbate URL in the tweet 4 | */ 5 | 6 | const blacklist = [ 7 | '1224895026318258177', // announcement that Mel was approved to stream on CB 8 | '1490488097250942976', // day-early 2 year anni announcement 9 | ]; 10 | 11 | module.exports = { 12 | blacklist 13 | } 14 | -------------------------------------------------------------------------------- /utils/tweetProcess.js: -------------------------------------------------------------------------------- 1 | const VOD = require('./VOD.js'); 2 | const debug = require('debug')('futureporn'); 3 | const projektMelodyTwitterId = require('./constants.js').projektMelodyTwitterId; 4 | const cbUrlRegex = /chaturbate\.com.*projektmelody/i 5 | 6 | const containsCBInviteLink = (tweet) => { 7 | try { 8 | if (tweet?.entities?.urls === undefined) return false; 9 | for (url of tweet.entities.urls) { 10 | if (url.unwound_url) { 11 | if (cbUrlRegex.test(url.unwound_url)) return true; 12 | else return false; 13 | } else { 14 | if (cbUrlRegex.test(url.expanded_url)) return true; 15 | else return false; 16 | } 17 | } 18 | } catch (e) { 19 | console.log('ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR') 20 | console.error(e); 21 | return false; 22 | } 23 | } 24 | 25 | 26 | const deriveTitle = (text) => { 27 | // greetz https://www.urlregex.com/ 28 | const urlRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/g; 29 | let title = text 30 | .replace(urlRegex, '') // remove urls 31 | .replace(/\n/g, ' ') // replace newlines with spaces 32 | .replace(/>/g, '>') // gimme dem greater-than brackets 33 | .replace(/</g, '<') // i want them less-thans too 34 | .replace(/&/g, '&') // ampersands are sexy 35 | .replace(/\s+$/, ''); // remove trailing whitespace 36 | return title; 37 | } 38 | 39 | 40 | 41 | /** 42 | * Does stuff with filtered tweets. (side-effects) 43 | */ 44 | const processTweet = async (tweet) => { 45 | debug(`processTweet() is as follows \n${JSON.stringify(tweet, 0, 2)}`); 46 | debug('>>> Processing Tweet'); 47 | debug(tweet); 48 | 49 | if (containsCBInviteLink(tweet)) { 50 | let tweetId = tweet.id; 51 | let tweetText = tweet.text; 52 | let date = tweet.created_at; 53 | let screenName = (tweet.author_id === projektMelodyTwitterId) ? 'ProjektMelody' : tweet.author_id; 54 | let announceUrl = `https://twitter.com/${screenName}/status/${tweetId}`; 55 | let announceTitle = deriveTitle(tweetText); 56 | 57 | console.log(`[*] Mel Chaturbate Invite Tweet Detected: ${announceUrl} at ${date}`); 58 | const vod = new VOD({ 59 | date, 60 | announceTitle, 61 | announceUrl 62 | }) 63 | vod.saveMarkdown(); 64 | } 65 | 66 | } 67 | 68 | 69 | module.exports = { 70 | deriveTitle, 71 | processTweet, 72 | containsCBInviteLink 73 | } -------------------------------------------------------------------------------- /utils/tweetprocess.test.js: -------------------------------------------------------------------------------- 1 | const { deriveTitle, processTweet, getFullTweetText } = require('./tweetProcess'); 2 | 3 | describe('scout', () => { 4 | describe('getFullTweetText', () => { 5 | test('should return the full text, regardless of tweet_text being truncated or not', () => { 6 | const sampleNotTruncatedTweet = { 7 | text: '@30_ppo おめでとう!\n' + 8 | 'あなたの勝利!\n' + 9 | '賀来賢人の“缶”敗です!\n' + 10 | '\n' + 11 | '@suntory のDMから\n' + 12 | '無料引換クーポンをゲットしよう! https://t.co/Z4uZsH9lsA', 13 | truncated: false 14 | }; 15 | const sampleTruncatedTweet = { 16 | text: '@sidd_sharma01 We will not be able to reveal the outcome of the investigation that is carried out internally, actio… https://t.co/1KvPmD9NAC', 17 | truncated: true, 18 | extended_tweet: { 19 | full_text: '@sidd_sharma01 We will not be able to reveal the outcome of the investigation that is carried out internally, action taken on the seller is completely internal information. Please keep us posted if the refund initiation is not fulfilled as suggested by our support team. ^MJ' 20 | } 21 | }; 22 | let fullText1 = getFullTweetText(sampleTruncatedTweet); 23 | expect(fullText1).toBe(sampleTruncatedTweet.extended_tweet.full_text); 24 | 25 | let fullText2 = getFullTweetText(sampleNotTruncatedTweet); 26 | expect(fullText2).toBe(sampleNotTruncatedTweet.text); 27 | }); 28 | }) 29 | describe('deriveTitle', () => { 30 | test('should get a title from a tweet\'s text', () => { 31 | const sampleText = 'don\'t look I shy...\n\nbut also here\'s the link to look\n\nhttps://chaturbate.com/projektmelody/?force=1&join_overlay=1&campaign=wXffl&disable_sound=0&tour=dT8X&room=projektmelody'; 32 | const title = deriveTitle(sampleText); 33 | expect(title).toBe('don\'t look I shy... but also here\'s the link to look'); 34 | }) 35 | }) 36 | }) -------------------------------------------------------------------------------- /utils/uploadProcess.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | module.exports = { 9 | upload 10 | } --------------------------------------------------------------------------------