├── .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 |
22 |
23 |
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 |
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 |
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 | Download & install both ipfs-kubo and ipfs-cluster-follow onto your server.
24 | Initialize your ipfs repo & start the ipfs daemon
25 |
26 | ipfs init
27 | ipfs daemon
28 |
29 |
30 | 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
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 |
46 |
47 |
48 |
Error
49 |
50 |
Home
51 |
52 |
53 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
LewdTuber *
19 |
20 |
21 | ProjektMelody
22 |
23 |
24 |
25 |
26 |
Stream Date *
27 |
28 |
29 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
Please log in to continue
49 |
50 |
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 | }
--------------------------------------------------------------------------------