Compare commits

...

2 Commits

Author SHA1 Message Date
CJ_Clippy 93b90c5586 progress
ci / build (push) Has been cancelled Details
2024-07-10 14:11:18 -08:00
CJ_Clippy 358e484a12 begin refactor for monorepo build 2024-07-09 18:34:23 -08:00
513 changed files with 11970 additions and 34371 deletions

View File

@ -19,9 +19,6 @@
**/charts/**/charts
**/.mocharc.json
**/.env
**/node_modules
packages/strapi/.tmp/
packages/strapi/.cache/
packages/strapi/.git/

View File

@ -6,7 +6,7 @@ namespaces:
./scripts/k8s-namespaces.sh
secrets:
dotenvx run -f .env.$(ENV) -- ./scripts/k8s-secrets.sh
dotenvx run -f .env.${ENV} -- ./scripts/k8s-secrets.sh
flux:
./scripts/flux-bootstrap.sh
@ -31,6 +31,9 @@ exoscale:
kind:
./scripts/kind-with-local-registry.sh
kindload:
./scripts/kind-load.sh
chisel:
./scripts/k8s-chisel-operator.sh

151
Tiltfile
View File

@ -1,4 +1,9 @@
# Tiltfile for working with Next and Strapi locally
## Tiltfile for working with Futureporn cluster locally
secret_settings(
disable_scrub=True
)
## cert-manager slows down Tilt updates so I prefer to keep it commented unless I specifically need to test certs
load('ext://cert_manager', 'deploy_cert_manager')
@ -7,13 +12,10 @@ deploy_cert_manager(
version='v1.15.1',
)
default_registry('localhost:5001')
load('ext://helm_remote', 'helm_remote')
# load('ext://dotenv', 'dotenv')
# dotenv(fn='.env')
# allow_k8s_contexts('vke-e41885d3-7f93-4f01-bfaa-426f20bf9f3f')
load('ext://dotenv', 'dotenv')
dotenv(fn='.env.development')
# helm_remote(
@ -46,19 +48,6 @@ load('ext://helm_remote', 'helm_remote')
# )
## this method results in the following error. Build Failed: Internal error occurred: failed calling webhook "webhook.cert-manager.io": failed to call webhook: Post "https://cert-manager-webhook.cert-manager.svc:443/validate?timeout=30s": service "cert-manager-webhook" not found
# helm_remote(
# 'cert-manager',
# repo_url='https://charts.jetstack.io',
# repo_name='cert-manager',
# namespace='cert-manager',
# version='1.15.1',
# set=[
# 'crds.enabled=true'
# ]
# )
helm_remote(
'traefik',
@ -113,55 +102,33 @@ helm_remote(
k8s_yaml(helm(
'./charts/fp',
values=['./charts/fp/values-dev.yaml'],
values=['./charts/fp/values.yaml'],
))
# k8s_yaml(helm(
# './charts/trigger',
# set=[
# 'trigger.name=trigger',
# 'trigger.replicaCount=2',
# 'trigger.image.tag=self-host-rc.2',
# 'trigger.image.pullPolicy=IfNotPresent',
# 'trigger.env.ENCRYPTION_KEY=%s' % os.getenv('TRIGGER_ENCRYPTION_KEY'),
# 'trigger.env.MAGIC_LINK_SECRET=%s' % os.getenv('TRIGGER_MAGIC_LINK_SECRET'),
# 'trigger.env.DATABASE_URL=%s' % os.getenv('TRIGGER_DATABASE_URL'),
# 'trigger.env.LOGIN_ORIGIN=%s' % os.getenv('TRIGGER_LOGIN_ORIGIN'),
# 'trigger.env.APP_ORIGIN=%s' % os.getenv('TRIGGER_APP_ORIGIN'),
# 'trigger.env.PORT=%s' % os.getenv('TRIGGER_PORT'),
# 'trigger.env.REMIX_APP_PORT=%s' % os.getenv('TRIGGER_REMIX_APP_PORT'),
# 'trigger.env.REDIS_HOST=redis-master.futureporn.svc.cluster.local',
# 'trigger.env.REDIS_PORT=6379',
# 'trigger.ingress.nginx.enabled=false',
# 'trigger.ingress.enabled=false',
# 'postgres.enabled=false'
# ]
# ))
# k8s_resource(
# workload='trigger',
# port_forwards=['3030'],
# )
# docker_build('fp/link2cid', './packages/link2cid')
docker_build(
'fp/strapi',
'.',
only=['./packages/strapi'],
dockerfile='./d.strapi.dev.dockerfile',
dockerfile='./d.packages.dockerfile',
target='strapi',
live_update=[
sync('./packages/strapi', '/app')
]
)
# docker_build(
# 'fp/bot',
# '.',
# only=['./packages/bot'],
# dockerfile='./d.bot.dockerfile',
# live_update=[
# sync('./packages/bot', '/app')
# ]
# )
docker_build(
'fp/bot',
'.',
dockerfile='./d.packages.dockerfile',
target='bot-dev',
live_update=[
sync('./packages/bot', '/app'),
run('cd /app && pnpm i', trigger=['./packages/bot/package.json', './packages/bot/pnpm-lock.yaml'])
]
)
@ -170,7 +137,7 @@ docker_build(
load('ext://uibutton', 'cmd_button')
cmd_button('postgres:create',
argv=['dotenvx', 'run', '-f', '.env.development', '--', 'sh', './scripts/postgres-create.sh'],
argv=['./scripts/postgres-create.sh'],
resource='postgres',
icon_name='dataset',
text='create (empty) databases',
@ -212,9 +179,8 @@ cmd_button('temporal-web:namespace',
docker_build(
'fp/next',
'.',
only=['./pnpm-lock.yaml', './package.json', './packages/next', './ca/letsencrypt-stg-root-x1.pem'],
dockerfile='d.next.dockerfile',
target='dev',
dockerfile='d.packages.dockerfile',
target='next',
build_args={
'NEXT_PUBLIC_STRAPI_URL': 'https://strapi.fp.sbtp.xyz'
},
@ -227,7 +193,6 @@ docker_build(
docker_build(
'fp/scout-manager',
'.',
only=['./pnpm-lock.yaml', './package.json', './packages/scout', './packages/next', './ca/letsencrypt-stg-root-x1.pem'],
dockerfile='d.packages.dockerfile',
target='scout-manager',
live_update=[
@ -238,13 +203,28 @@ docker_build(
# entrypoint='pnpm tsx watch ./src/index.ts'
)
docker_build(
'fp/boop',
'.',
dockerfile='d.packages.dockerfile',
target='boop',
live_update=[
sync('./packages/boop', '/app'),
# run('cd /app && pnpm i', trigger=['./packages/boop/package.json', './packages/boop/pnpm-lock.yaml']),
],
# entrypoint='pnpm nodemon --ext js,ts,json,yaml --exec node --no-warnings=ExperimentalWarning --loader ts-node/esm ./src/index.ts'
# entrypoint='pnpm tsx watch ./src/index.ts'
)
docker_build(
'fp/scout-worker',
'.',
only=['./pnpm-lock.yaml', './package.json', './packages/scout', './packages/next', './ca/letsencrypt-stg-root-x1.pem'],
# ignore=['./packages/next'], # I wish I could use this ignore to ignore file changes in this dir, but that's not how it works
dockerfile='d.packages.dockerfile',
target='scout:worker',
target='scout-worker',
live_update=[
# idk if this run() is effective
# run('cd /app && pnpm i', trigger=['./packages/scout/package.json', './packages/scout/pnpm-lock.yaml']),
@ -259,6 +239,10 @@ docker_build(
# this entrypoint is a godsend. It lets me restart the node app (fast) without having to rebuild the docker container (slow)
entrypoint='pnpm nodemon --ext js,ts,json,yaml --exec node --no-warnings=ExperimentalWarning --loader ts-node/esm ./src/temporal/worker.ts'
)
# k8s_resource(
# workload='kubernetes-ingress-controller-manager',
# links=[
@ -336,6 +320,11 @@ k8s_resource(
labels=['backend'],
)
# k8s_resource(
# workload='',
# )
# k8s_resource(
# workload='pgadmin',
# port_forwards=['5050'],
@ -387,8 +376,8 @@ helm_remote(
namespace='futureporn',
version='0.37.0',
set=[
'admintools.image.tag=1.24.1-tctl-1.18.1-cli-0.12.0',
'web.image.tag=2.27.2',
'admintools.image.tag=1.24.2-tctl-1.18.1-cli-0.13.0',
'web.image.tag=2.28.0',
'prometheus.enabled=false',
'grafana.enabled=false',
'elasticsearch.enabled=false',
@ -421,7 +410,7 @@ k8s_resource(
])
k8s_resource(
workload='temporal-frontend',
labels='temporal', port_forwards=['7233'],
labels='temporal',
resource_deps=[
'postgres',
'strapi'
@ -485,24 +474,18 @@ k8s_resource(
labels=['backend']
)
# k8s_resource(
# workload='bot',
# labels=['backend']
# )
k8s_resource(
workload='bot',
labels=['backend'],
resource_deps=['strapi', 'temporal-web'],
)
# k8s_resource(
# workload='cert-manager',
# labels='cert-manager'
# )
# k8s_resource(
# workload='cert-manager-webhook',
# labels='cert-manager'
# )
# k8s_resource(
# workload='cert-manager-cainjector',
# labels='cert-manager'
# )
# k8s_resource(
# workload='cert-manager-startupapicheck',
# labels='cert-manager'
# workload='trigger',
# labels=['backend'],
# port_forwards=['3030:3000'],
# resource_deps=['postgres', 'redis-master'],
# links=[
# link('http://localhost:3030')
# ],
# )

View File

@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Pod
metadata:
name: boop
namespace: futureporn
labels:
app.kubernetes.io/name: boop
spec:
containers:
- name: boop
image: fp/boop
resources: {}
restartPolicy: OnFailure

View File

@ -0,0 +1,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bot
namespace: futureporn
labels:
app: bot
spec:
replicas: {{ .Values.scout.worker.replicas }}
selector:
matchLabels:
app: bot
template:
metadata:
labels:
app: bot
spec:
containers:
- name: bot
image: "{{ .Values.bot.imageName }}"
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: DISCORD_CHANNEL_ID
value: "{{ .Values.bot.discordChannelId }}"
- name: DISCORD_TOKEN
valueFrom:
secretKeyRef:
name: discord
key: token
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "250m"
memory: "256Mi"

View File

@ -14,7 +14,7 @@ spec:
- name: HOSTNAME
value: 0.0.0.0
- name: NEXT_PUBLIC_UPPY_COMPANION_URL
value: "{{ .Values.uppy.hostname }}"
value: "{{ .Values.uppy.url }}"
ports:
- name: web
containerPort: 3000

View File

@ -98,8 +98,10 @@ spec:
value: "{{ .Values.uppy.s3.bucket }}"
- name: COMPANION_AWS_REGION
value: "{{ .Values.uppy.s3.region }}"
- name: COMPANION_AWS_PREFIX
value: "{{ .Values.uppy.s3.prefix }}"
- name: COMPANION_AWS_ENDPOINT
value: "{{ .Values.uppy.s3.endpoint }}"
# - name: COMPANION_AWS_PREFIX
# value: "{{ .Values.uppy.s3.prefix }}"
## COMPANION_OAUTH_DOMAIN is only necessary if using a different domain per each uppy pod.
## We don't need this because we are load balancing the pods so they all use the same domain name.

View File

@ -1,31 +0,0 @@
environment: production
storageClassName: vultr-block-storage-hdd
link2cid:
imageName: gitea.futureporn.net/futureporn/link2cid:latest
scout:
manager:
imageName: gitea.futureporn.net/futureporn/scout-manager:latest
worker:
imageName: gitea.futureporn.net/futureporn/scout-worker:latest
replicas: 2
pubsubServerUrl: https://realtime.futureporn.net/faye
cdnBucketUrl: https://futureporn-b2.b-cdn.net
s3BucketName: futureporn
next:
imageName: gitea.futureporn.net/futureporn/next:latest
hostname: next.sbtp.xyz
capture:
imageName: gitea.futureporn.net/futureporn/capture:latest
strapi:
imageName: sjc.vultrcr.com/fpcontainers/strapi
port: 1339
url: https://portal.futureporn.net
hostname: strapi.sbtp.xyz
ingressClassName: traefik
managedBy: Helm
adminEmail: cj@futureporn.net
extraArgs:
- --dns01-recursive-nameservers-only
- --dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53
certManager:
issuer: letsencrypt-production

View File

@ -38,12 +38,22 @@ uppy:
hostname: uppy.fp.sbtp.xyz
imageName: fp/uppy
s3:
endpoint: s3.us-west-000.backblazeb2.com
bucket: futureporn-usc
endpoint: https://s3.us-west-000.backblazeb2.com
bucket: fp-usc-dev
region: us-west-000
prefix: s3
clientOrigins: next.fp.sbtp.xyz
domain: uppy.fp.sbtp.xyz
uploadUrls: https://uppy.fp.sbtp.xyz/files
url: https://uppy.fp.sbtp.xyz
certManager:
issuer: letsencrypt-staging
issuer: letsencrypt-staging
bot:
discordChannelId: "1185024773231759481"
imageName: fp/bot
trigger:
imageName: ghcr.io/triggerdotdev/trigger.dev:self-host-rc.3
worker:
replicas: 2
webapp:
replicas: 1

1
charts/trigger/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
charts/

View File

@ -0,0 +1,24 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
node_modules/

View File

@ -0,0 +1,2 @@
digest: sha256:e439e4b30ba18357defec97ba080973743a4724c423b78913990409f78f1ebd8
generated: "2023-10-20T14:22:57.044126+05:30"

24
charts/trigger/Chart.yaml Normal file
View File

@ -0,0 +1,24 @@
apiVersion: v2
name: trigger
description: A Helm chart for a full Trigger application stack
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

3
charts/trigger/README.md Normal file
View File

@ -0,0 +1,3 @@
# Trigger.dev Helm Chart
@see https://github.com/triggerdotdev/trigger.dev/tree/main/helm-charts

203
charts/trigger/package-lock.json generated Normal file
View File

@ -0,0 +1,203 @@
{
"name": "helm-charts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "helm-charts",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@bitnami/readme-generator-for-helm": "^2.6.0"
}
},
"node_modules/@bitnami/readme-generator-for-helm": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@bitnami/readme-generator-for-helm/-/readme-generator-for-helm-2.6.0.tgz",
"integrity": "sha512-LcByNCryaC2OJExL9rnhyFJ18+vrZu1gVoN2Z7j/HI42EjV4kLgT4G1KEPNnrKbls9HvozBqMG+sKZIDh0McFg==",
"dependencies": {
"commander": "^7.1.0",
"dot-object": "^2.1.4",
"lodash": "^4.17.21",
"markdown-table": "^2.0.0",
"yaml": "^2.0.0-3"
},
"bin": {
"readme-generator": "bin/index.js"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"engines": {
"node": ">= 10"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/dot-object": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz",
"integrity": "sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA==",
"dependencies": {
"commander": "^4.0.0",
"glob": "^7.1.5"
},
"bin": {
"dot-object": "bin/dot-object"
}
},
"node_modules/dot-object/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/markdown-table": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
"integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==",
"dependencies": {
"repeat-string": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/repeat-string": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yaml": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
"integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
"engines": {
"node": ">= 14"
}
},
"readme-generator-for-helm": {
"version": "2.6.1",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"commander": "^7.1.0",
"dot-object": "^2.1.4",
"lodash": "^4.17.21",
"markdown-table": "^2.0.0",
"yaml": "^2.0.0-3"
},
"bin": {
"readme-generator": "bin/index.js"
},
"devDependencies": {
"eslint": "^7.24.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.22.1",
"jest": "^29.2.1",
"temp": "^0.9.4"
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"name": "helm-charts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate-docs": "readme-generator --readme README.md --values values.yaml"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@bitnami/readme-generator-for-helm": "^2.6.0"
}
}

View File

@ -0,0 +1,71 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "trigger.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "trigger.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create unified labels for trigger components
*/}}
{{- define "trigger.common.matchLabels" -}}
app: {{ template "trigger.name" . }}
release: {{ .Release.Name }}
{{- end -}}
{{- define "trigger.common.metaLabels" -}}
chart: {{ template "trigger.chart" . }}
heritage: {{ .Release.Service }}
{{- end -}}
{{- define "trigger.common.labels" -}}
{{ include "trigger.common.matchLabels" . }}
{{ include "trigger.common.metaLabels" . }}
{{- end -}}
{{- define "trigger.labels" -}}
{{ include "trigger.matchLabels" . }}
{{ include "trigger.common.metaLabels" . }}
{{- end -}}
{{- define "trigger.matchLabels" -}}
component: {{ .Values.trigger.name | quote }}
{{ include "trigger.common.matchLabels" . }}
{{- end -}}
{{/*
Create a fully qualified postgresql name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "trigger.postgresql.hostname" -}}
{{- if .Values.postgresql.fullnameOverride -}}
{{- .Values.postgresql.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- printf "%s-%s" .Release.Name .Values.postgresql.name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s-%s" .Release.Name $name .Values.postgresql.name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create the postgresql connection string.
*/}}
{{- define "trigger.postgresql.connectionString" -}}
{{- $host := include "trigger.postgresql.hostname" . -}}
{{- $port := 5432 -}}
{{- $username := .Values.postgresql.global.postgresql.postgresqlUsername | default "postgres" -}}
{{- $password := .Values.postgresql.global.postgresql.postgresqlPassword | default "password" -}}
{{- $database := .Values.postgresql.global.postgresql.postgresqlDatabase | default "trigger" -}}
{{- $connectionString := printf "postgresql://%s:%s@%s:%d/%s" $username $password $host $port $database -}}
{{- printf "%s" $connectionString -}}
{{- end -}}

View File

@ -0,0 +1,43 @@
{{ if .Values.ingress.enabled }}
{{- $ingress := .Values.ingress }}
{{- if and $ingress.ingressClassName (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey $ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set $ingress.annotations "kubernetes.io/ingress.class" $ingress.ingressClassName}}
{{- end }}
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: trigger-ingress
{{- with $ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and $ingress.ingressClassName (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ $ingress.ingressClassName | default "nginx" }}
{{- end }}
{{- if $ingress.tls }}
tls:
{{- range $ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- http:
paths:
- path: {{ $ingress.trigger.path }}
pathType: {{ $ingress.trigger.pathType }}
backend:
service:
name: {{ include "trigger.name" . }}
port:
number: 3000
{{- if $ingress.hostName }}
host: {{ $ingress.hostName }}
{{- end }}
{{ end }}

View File

@ -0,0 +1,95 @@
{{- $trigger := .Values.trigger -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "trigger.name" . }}
namespace: futureporn
annotations:
updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
{{- with $trigger.deploymentAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "trigger.labels" . | nindent 4 }}
spec:
replicas: {{ $trigger.replicaCount }}
selector:
matchLabels:
{{- include "trigger.matchLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "trigger.matchLabels" . | nindent 8 }}
annotations:
updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
{{- with $trigger.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with $trigger.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ $trigger.name }}
image: "{{ $trigger.image.repository }}:{{ $trigger.image.tag | default "latest" }}"
imagePullPolicy: {{ $trigger.image.pullPolicy }}
ports:
- name: http
containerPort: 3000
protocol: TCP
readinessProbe:
httpGet:
path: /
port: 3000
envFrom:
- secretRef:
name: {{ $trigger.kubeSecretRef | default (include "trigger.name" .) }}
{{- if $trigger.resources }}
resources: {{- toYaml $trigger.resources | nindent 12 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: trigger
labels:
annotations:
spec:
type: {{ $trigger.service.type }}
selector:
{{- include "trigger.matchLabels" . | nindent 8 }}
ports:
- port: 3000
targetPort: 3000
protocol: TCP
{{- if eq $trigger.service.type "NodePort" }}
nodePort: {{ $trigger.service.nodePort }}
{{- end }}
---
{{ if not $trigger.kubeSecretRef }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "trigger.name" . }}
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
stringData:
{{- $requiredVars := dict "MAGIC_LINK_SECRET" (randAlphaNum 32 | lower)
"SESSION_SECRET" (randAlphaNum 32 | lower)
"ENCRYPTION_KEY" (randAlphaNum 32 | lower)
"DIRECT_URL" (include "trigger.postgresql.connectionString" .)
"DATABASE_URL" (include "trigger.postgresql.connectionString" .) }}
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (include "trigger.name" .)) | default dict }}
{{- $secretData := (get $secretObj "data") | default dict }}
{{ range $key, $value := .Values.trigger.env }}
{{- $default := get $requiredVars $key -}}
{{- $current := get $secretData $key | b64dec -}}
{{- $v := $value | default ($current | default $default) -}}
{{ $key }}: {{ $v | quote }}
{{ end -}}
{{- end }}

276
charts/trigger/values.yaml Normal file
View File

@ -0,0 +1,276 @@
# Default values for helm-charts.
# This is a YAML-formatted file.
## @section Common parameters
##
## @param nameOverride Override release name
##
nameOverride: ""
## @param fullnameOverride Override release fullname
##
fullnameOverride: ""
## @section Trigger.dev parameters
##
trigger:
## @param trigger.name
name: trigger
## @param trigger.fullnameOverride trigger fullnameOverride
##
fullnameOverride: ""
## @param trigger.podAnnotations trigger pod annotations
##
podAnnotations: {}
## @param trigger.deploymentAnnotations trigger deployment annotations
##
deploymentAnnotations: {}
## @param trigger.replicaCount trigger replica count
##
replicaCount: 2
## trigger image parameters
##
image:
## @param trigger.image.repository trigger image repository
##
repository: ghcr.io/triggerdotdev/trigger.dev
## @param trigger.image.tag trigger image tag
##
tag: "latest"
## @param trigger.image.pullPolicy trigger image pullPolicy
##
pullPolicy: Always
## @param trigger.resources.limits.memory container memory limit [(docs)](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
## @param trigger.resources.requests.cpu container CPU requests [(docs)](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
##
resources:
limits:
memory: 800Mi
requests:
cpu: 250m
## @param trigger.affinity Backend pod affinity
##
affinity: {}
## @param trigger.kubeSecretRef trigger secret resource reference name
##
kubeSecretRef: ""
## trigger service
##
service:
## @param trigger.service.annotations trigger service annotations
##
annotations: {}
## @param trigger.service.type trigger service type
##
type: ClusterIP
## @param trigger.service.nodePort trigger service nodePort (used if above type is `NodePort`)
##
nodePort: ""
## @skip trigger.env
##
env:
ENCRYPTION_KEY: ""
MAGIC_LINK_SECRET: ""
SESSION_SECRET: ""
LOGIN_ORIGIN: ""
APP_ORIGIN: ""
DIRECT_URL: ""
DATABASE_URL: ""
FROM_EMAIL: ""
REPLY_TO_EMAIL: ""
RESEND_API_KEY: ""
AUTH_GITHUB_CLIENT_ID: ""
AUTH_GITHUB_CLIENT_SECRET: ""
## @section Postgres parameters
## Documentation: https://github.com/bitnami/charts/tree/main/bitnami/postgresql-ha
##
postgresql:
## @param postgresql.enabled Enable Postgres
##
enabled: true
## @param postgresql.name Name used to build variables (deprecated)
##
name: "postgresql"
## @param postgresql.nameOverride Name override
##
nameOverride: "postgresql"
## @param postgresql.fullnameOverride Fullname override
##
fullnameOverride: "postgresql"
global:
postgresql:
## @param postgresql.global.postgresql.auth.postgresPassword Password for the "postgres" admin user (overrides `auth.postgresPassword`)
## @param postgresql.global.postgresql.auth.username Name for a custom user to create (overrides `auth.username`)
## @param postgresql.global.postgresql.auth.password Password for the custom user to create (overrides `auth.password`)
## @param postgresql.global.postgresql.auth.database Name for a custom database to create (overrides `auth.database`)
## @param postgresql.global.postgresql.auth.existingSecret Name of existing secret to use for PostgreSQL credentials (overrides `auth.existingSecret`).
## @param postgresql.global.postgresql.auth.secretKeys.adminPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.adminPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set.
## @param postgresql.global.postgresql.auth.secretKeys.userPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.userPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set.
## @param postgresql.global.postgresql.auth.secretKeys.replicationPasswordKey Name of key in existing secret to use for PostgreSQL credentials (overrides `auth.secretKeys.replicationPasswordKey`). Only used when `postgresql.global.postgresql.auth.existingSecret` is set.
##
auth:
postgresPassword: "password"
username: "postgres"
password: "password"
database: "trigger"
existingSecret: ""
secretKeys:
adminPasswordKey: ""
userPasswordKey: ""
replicationPasswordKey: ""
## @param postgresql.global.postgresql.service.ports.postgresql PostgreSQL service port (overrides `service.ports.postgresql`)
##
service:
ports:
postgresql: "5432"
## Bitnami PostgreSQL image version
## ref: https://hub.docker.com/r/bitnami/postgresql/tags/
## @param postgresql.image.registry PostgreSQL image registry
## @param postgresql.image.repository PostgreSQL image repository
## @param postgresql.image.tag PostgreSQL image tag (immutable tags are recommended)
## @param postgresql.image.digest PostgreSQL image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag
## @param postgresql.image.pullPolicy PostgreSQL image pull policy
## @param postgresql.image.pullSecrets Specify image pull secrets
## @param postgresql.image.debug Specify if debug values should be set
##
image:
registry: docker.io
repository: bitnami/postgresql
tag: 14.10.0-debian-11-r21
digest: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
## Set to true if you would like to see extra information on logs
##
debug: false
## @param postgresql.architecture PostgreSQL architecture (`standalone` or `replication`)
##
architecture: standalone
## Replication configuration
## Ignored if `postgresql.architecture` is `standalone`
##
## @param postgresql.containerPorts.postgresql PostgreSQL container port
##
containerPorts:
postgresql: 5432
## @param postgresql.postgresqlDataDir PostgreSQL data dir
##
postgresqlDataDir: /bitnami/postgresql/data
## @param postgresql.postgresqlSharedPreloadLibraries Shared preload libraries (comma-separated list)
##
postgresqlSharedPreloadLibraries: "pgaudit"
## @section PostgreSQL Primary parameters
##
primary:
## Configure extra options for PostgreSQL Primary containers' liveness, readiness and startup probes
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes
## @param postgresql.primary.livenessProbe.enabled Enable livenessProbe on PostgreSQL Primary containers
## @param postgresql.primary.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe
## @param postgresql.primary.livenessProbe.periodSeconds Period seconds for livenessProbe
## @param postgresql.primary.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe
## @param postgresql.primary.livenessProbe.failureThreshold Failure threshold for livenessProbe
## @param postgresql.primary.livenessProbe.successThreshold Success threshold for livenessProbe
##
livenessProbe:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
## @param postgresql.primary.readinessProbe.enabled Enable readinessProbe on PostgreSQL Primary containers
## @param postgresql.primary.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe
## @param postgresql.primary.readinessProbe.periodSeconds Period seconds for readinessProbe
## @param postgresql.primary.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe
## @param postgresql.primary.readinessProbe.failureThreshold Failure threshold for readinessProbe
## @param postgresql.primary.readinessProbe.successThreshold Success threshold for readinessProbe
##
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
## @param postgresql.primary.startupProbe.enabled Enable startupProbe on PostgreSQL Primary containers
## @param postgresql.primary.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe
## @param postgresql.primary.startupProbe.periodSeconds Period seconds for startupProbe
## @param postgresql.primary.startupProbe.timeoutSeconds Timeout seconds for startupProbe
## @param postgresql.primary.startupProbe.failureThreshold Failure threshold for startupProbe
## @param postgresql.primary.startupProbe.successThreshold Success threshold for startupProbe
##
startupProbe:
enabled: false
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 15
successThreshold: 1
persistence:
## @param postgresql.primary.persistence.enabled Enable PostgreSQL Primary data persistence using PVC
##
enabled: true
## @param postgresql.primary.persistence.existingClaim Name of an existing PVC to use
##
existingClaim: ""
## @param postgresql.primary.persistence.accessModes PVC Access Mode for PostgreSQL volume
##
accessModes:
- ReadWriteOnce
## @param postgresql.primary.persistence.size PVC Storage Request for PostgreSQL volume
##
size: 8Gi
## @section Ingress parameters
## Documentation: https://kubernetes.io/docs/concepts/services-networking/ingress/
##
ingress:
## @param ingress.enabled Enable ingress
##
enabled: true
## @param ingress.ingressClassName Ingress class name
##
ingressClassName: nginx
## @param ingress.nginx.enabled Ingress controller
##
nginx:
enabled: false
## @param ingress.annotations Ingress annotations
##
annotations:
{}
# kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
## @param ingress.hostName Ingress hostname (your custom domain name, e.g. `infisical.example.org`)
## Replace with your own domain
##
hostName: ""
## @param ingress.tls Ingress TLS hosts (matching above hostName)
## Replace with your own domain
##
tls:
[]
# - secretName: letsencrypt-nginx
# hosts:
# - infisical.local
## @param ingress.trigger.path Trigger.dev ingress path
## @param ingress.trigger.pathType Trigger.dev ingress path type
##
trigger:
path: /
pathType: Prefix

View File

@ -1,20 +0,0 @@
FROM node:20 AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
ENV NODE_ENV=production
COPY ./packages/bot /usr/src/app
WORKDIR /usr/src/app
RUN mkdir -p /prod/scout
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm deploy --filter=bot --prod /prod/scout
FROM base AS bot
COPY --from=build /prod/bot /app
WORKDIR /app
ENTRYPOINT ["pnpm"]
CMD ["run", "start"]

View File

@ -1,66 +0,0 @@
## Important! Build context is the ROOT of the project.
## this keeps the door open for future possibility of shared code between pnpm workspace packages
FROM node:20-slim AS base
FROM base AS deps
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
FROM deps AS install
ARG NEXT_PUBLIC_SITE_URL=https://futureporn.net
ARG NEXT_PUBLIC_STRAPI_URL=https://portal.futureporn.net
ARG NEXT_PUBLIC_UPPY_COMPANION_URL=https://uppy.futureporn.net
ENV NEXT_PUBLIC_SITE_URL ${NEXT_PUBLIC_SITE_URL}
ENV NEXT_PUBLIC_STRAPI_URL ${NEXT_PUBLIC_STRAPI_URL}
ENV NEXT_PUBLIC_UPPY_COMPANION_URL ${NEXT_PUBLIC_UPPY_COMPANION_URL}
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_EXTRA_CA_CERTS "/app/letsencrypt-stg-root-x1.pem"
COPY pnpm-lock.yaml ./
RUN pnpm fetch
COPY ./packages/next /app
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install
FROM install AS dev
CMD ["pnpm", "run", "dev"]
FROM install AS build
RUN pnpm run build
# COPY --chown=node:node --from=install /app/package.json /app/pnpm-lock.yaml ./
# RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
# COPY --from=install /app /app # i think this is duplicate
# can't get these to work because errors like "/prod/next/.next/standalone": not found
# as if pnpm is not copying the build artifacts.
# also this makes the build REALLY slow (adds ~10mins to build time)
# RUN pnpm deploy --filter=@futureporn/next --prod /prod/next
# RUN pnpm deploy --filter=@futureporn/link2cid --prod /prod/link2cid
# FROM deps as release
# # ENV NEXT_SHARP_PATH=/app/node_modules/sharp
# ENV NODE_ENV=production
# WORKDIR /app
# COPY --from=build /app/public ./public
# COPY --from=build /app/.next/standalone ./
# COPY --from=build /app/.next/static ./.next/static
# CMD [ "dumb-init", "node", "server.js" ]
FROM deps AS next
RUN apt-get update && apt-get install -y -qq --no-install-recommends dumb-init
COPY --chown=node:node --from=build /app/package.json /app/pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
COPY --chown=node:node --from=build /app/public ./public
COPY --chown=node:node --from=build /app/.next/standalone ./
COPY --chown=node:node --from=build /app/.next/static ./.next/static
ENV TZ=UTC
ENV NODE_ENV=production
ENV HOSTNAME="0.0.0.0"
CMD [ "dumb-init", "node", "server.js" ]

View File

@ -2,7 +2,7 @@
## Because we are using monorepo with pnpm workspaces, we have many npm packages in this single git repo.
## Some of these packages in the monorepo depend on other packages in the monorepo.
## In order to build these individual packages which inter-depend on eachother,
## all of the dependent code must be present in the build.
## all of the dependent code must be present in the build context.
##
## Below, COPY . /usr/src/app copies all the app code into the build context.
## Because we use Tilt, only specific path directories are visible to docker. This helps with build performance.
@ -16,27 +16,131 @@ FROM node:20 AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
ENTRYPOINT ["pnpm"]
FROM base AS build
ENV NODE_ENV=production
ARG NEXT_PUBLIC_SITE_URL=https://futureporn.net
ARG NEXT_PUBLIC_STRAPI_URL=https://portal.futureporn.net
ARG NEXT_PUBLIC_UPPY_COMPANION_URL=https://uppy.futureporn.net
ENV NEXT_PUBLIC_SITE_URL ${NEXT_PUBLIC_SITE_URL}
ENV NEXT_PUBLIC_STRAPI_URL ${NEXT_PUBLIC_STRAPI_URL}
ENV NEXT_PUBLIC_UPPY_COMPANION_URL ${NEXT_PUBLIC_UPPY_COMPANION_URL}
ENV NEXT_TELEMETRY_DISABLED 1
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN mkdir -p /prod/scout
RUN mkdir -p /prod
RUN pnpm fetch
## strapi needs node-gyp
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install -g node-gyp
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
ENV NODE_ENV production
RUN pnpm --recursive build
## we use pnpm deploy to create bundled, isolated node packages for docker containers. See https://pnpm.io/cli/deploy
## we can deploy most packages this way, but with some projects such as next it's not necessary as next's build step packages all dependencies.
RUN pnpm deploy --filter=boop --prod /prod/boop
RUN pnpm deploy --filter=scout --prod /prod/scout
# RUN pnpm deploy --filter=bot --prod /prod/bot
FROM base AS scout-manager
COPY --from=build /prod/scout /app
WORKDIR /app
ENTRYPOINT ["pnpm"]
CMD ["run", "start:manager"]
FROM base AS strapi
COPY --from=build /usr/src/app/packages/strapi .
CMD ["run", "develop"]
# NODE_ENV=development is important for the fp/next build.
## @todo remove the --filter
FROM base AS boop
COPY --from=build /prod/boop /app
CMD ["start"]
# COPY pnpm-lock.yaml ./
# RUN pnpm fetch
# COPY ./packages/next /app
# RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install
# FROM base AS next-prep
# ARG NEXT_PUBLIC_SITE_URL=https://futureporn.net
# ARG NEXT_PUBLIC_STRAPI_URL=https://portal.futureporn.net
# ARG NEXT_PUBLIC_UPPY_COMPANION_URL=https://uppy.futureporn.net
# ENV NEXT_PUBLIC_SITE_URL ${NEXT_PUBLIC_SITE_URL}
# ENV NEXT_PUBLIC_STRAPI_URL ${NEXT_PUBLIC_STRAPI_URL}
# ENV NEXT_PUBLIC_UPPY_COMPANION_URL ${NEXT_PUBLIC_UPPY_COMPANION_URL}
# ENV NEXT_TELEMETRY_DISABLED 1
# COPY pnpm-lock.yaml ./
# COPY ./packages/next /app
# RUN pnpm fetch
# RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install
# FROM base AS next-build
# COPY --from=build /prod/next /app
# # RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install
# # RUN ls -la
# RUN pnpm run build
FROM base as next
RUN apt-get update && apt-get install -y -qq --no-install-recommends dumb-init
COPY --chown=node:node --from=build /usr/src/app/packages/next/public ./public
COPY --chown=node:node --from=build /usr/src/app/packages/next/.next/standalone ./
COPY --chown=node:node --from=build /usr/src/app/packages/next/.next/static ./.next/static
ENV TZ=UTC
ENV NODE_ENV=production
ENV HOSTNAME="0.0.0.0"
ENTRYPOINT [ "dumb-init", "node", "server.js" ]
# FROM base AS next-pre
# COPY --from=build /prod/next /app
# ENV NODE_EXTRA_CA_CERTS "/app/letsencrypt-stg-root-x1.pem"
# FROM next-pre AS next-dev
# CMD ["pnpm", "run", "dev"]
# FROM next-pre AS next-build
# RUN pnpm run build
# FROM base AS next
# RUN apt-get update && apt-get install -y -qq --no-install-recommends dumb-init
# # COPY --chown=node:node --from=build /prod/next .
# COPY --chown=node:node --from=next-build /app/package.json /app/pnpm-lock.yaml ./
# RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
# COPY --chown=node:node --from=next-build /app/public ./public
# COPY --chown=node:node --from=next-build /app/.next/standalone ./
# COPY --chown=node:node --from=next-build /app/.next/static ./.next/static
# RUN ls -la .
# ENV TZ=UTC
# ENV NODE_ENV=production
# ENV HOSTNAME="0.0.0.0"
# ENTRYPOINT [ "dumb-init", "node", "server.js" ]
# FROM base AS scout-manager
# COPY --from=build /prod/scout /app
# CMD ["run", "start:manager"]
# FROM base AS scout-worker
# COPY --from=build /prod/scout /app
# COPY --from=build /usr/src/app/certs/letsencrypt-stg-root-x1.pem /app
# ENV NODE_EXTRA_CA_CERTS "/app/certs/letsencrypt-stg-root-x1.pem"
# CMD ["run", "start:worker"]
# FROM base AS temporal-worker
# COPY --from=build /prod/temporal-worker /app
# CMD ["run", "start"]
# FROM base AS bot-prep
# COPY --from=build /prod/bot /app
# FROM bot-prep AS bot
# CMD ["run", "start"]
# FROM bot-prep AS bot-dev
# CMD ["run", "dev"]
FROM base AS scout-worker
COPY --from=build /prod/scout /app
COPY --from=build /usr/src/app/certs/letsencrypt-stg-root-x1.pem
ENV NODE_EXTRA_CA_CERTS "/app/certs/letsencrypt-stg-root-x1.pem"
WORKDIR /app
ENTRYPOINT ["pnpm"]
CMD ["run", "start:worker"]

View File

@ -1,10 +0,0 @@
FROM node:18 AS base
WORKDIR /usr/src/app/
RUN corepack enable
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
COPY ./packages/strapi/package.json ./packages/strapi/pnpm-lock.yaml .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY ./packages/strapi/ .
RUN ["pnpm", "run", "build"]
CMD ["pnpm", "run", "develop"]

4
packages/archive/.npmrc Normal file
View File

@ -0,0 +1,4 @@
engine-strict=true
package-manager-strict=true
use-node-version=20.13.1
node-version=20.13.1

View File

@ -0,0 +1,4 @@
# archive
This module does vod processing on the backend.

4
packages/archive/node.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface ImportMeta {
dirname: string;
url: string;
}

View File

@ -0,0 +1,16 @@
{
"name": "archive",
"version": "0.0.1",
"description": "",
"main": "index.ts",
"scripts": {
"test": "mocha"
},
"keywords": [],
"author": "@CJ_Clippy",
"license": "Unlicense",
"dependencies": {
"@aws-sdk/client-s3": "^3.583.0",
"prevvy": "^7.0.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,240 @@
import Prevvy from 'prevvy';
import path from 'node:path';
import os from 'node:os';
import { promisify } from 'util';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
type CVod = {
id: number,
attributes: {
date: string,
note: null,
date2: string,
title: string,
vtuber: {
data: {
id: number,
attributes: {
slug: string,
image: string,
imageBlur: string,
displayName: string
}
}
},
chatLog: null,
muxAsset: {
data: {
assetId: string,
playbackId: string
}
},
spoilers: null,
thinHash: null,
createdAt: null,
thiccHash: null,
thumbnail: {
data: {
id: number,
attributes: {
url: string,
cdnUrl: string
}
}
},
updatedAt: string,
videoSrcB2: {
data: {
id: number,
attributes: {
key: string,
url: string,
cdnUrl: string,
uploadId: string
}
}
},
announceUrl: string,
publishedAt: string,
video240Hash: string,
video360Hash: null,
video480Hash: null,
video720Hash: null,
videoSrcHash: string,
announceTitle: string,
archiveStatus: null,
tagVodRelations: {
data: any[]
}
}
}
type CStrapi = {
strapiUrl: string,
strapiApiKey: string
}
type S3 = {
port: number,
bucket: string,
region: string,
useSSL: boolean,
endPoint: string,
accessKey: string,
pathStyle: boolean,
secretKey: string
}
type CBunnyPullZone = {
cdnHostname: string
}
interface IUploadData {
uploadId: string;
key: string;
url: string;
}
export async function __generateThumbnail(vod: CVod): Promise<string> {
const fileName = `vod-${vod?.id}-thumb.png`;
const thumbnailFilePath = path.join(os.tmpdir(), fileName);
const videoInputUrl = vod.attributes.videoSrcB2?.data?.attributes?.cdnUrl;
if (!videoInputUrl) {
console.error(vod?.attributes?.videoSrcB2);
throw new Error(`videoInputUrl in __generateThumbnail was undefined`);
}
console.log(`🫰 Creating thumbnail from ${videoInputUrl} ---> ${thumbnailFilePath}`);
const thumb = new Prevvy({
input: videoInputUrl,
output: thumbnailFilePath,
throttleTimeout: 2000,
width: 128,
cols: 5,
rows: 5,
});
thumb.on('progress', async (data: { percentage: number }) => {
console.log(`Thumbnail generation ${data.percentage}%`);
});
await thumb.generate();
return thumbnailFilePath;
}
function createId(): string {
const timestamp: number = new Date().getTime();
const randomPart: number = Math.floor(Math.random() * 10000);
return `${timestamp}-${randomPart}`;
}
export async function uploadToB2 (s3Resource: S3, filePath: string): IUploadData {
const { bucket, endPoint, region, accessKey, secretKey } = s3Resource;
const keyName = `${createId()}-${path.basename(filePath)}`
console.log(`uploadToB2 begin. bucket:${bucket} endpoint:${endPoint}`)
const urlPrefix = 'https://f000.backblazeb2.com/b2api/v1/b2_download_file_by_id?fileId='
const s3 = new S3Client({
endpoint: `https://${endPoint}`,
region: region,
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretKey,
}
});
const file = Bun.file(filePath);
const fileStream = await file.arrayBuffer();
var params = {Bucket: bucket, Key: keyName, Body: fileStream};
const res = await s3.send(new PutObjectCommand(params));
if (!res.VersionId) {
const msg = 'res was missing VersionId'
throw new Error(msg)
}
const url = `${urlPrefix}${res.VersionId}`;
const blah: IUploadData = {
uploadId: res.VersionId,
key: keyName,
url: url
}
console.log(url)
console.log(blah)
return blah;
}
export async function associateB2WithVod(vod: IVod, strapi: CStrapi, uploadData: IUploadData, zone: CBunnyPullZone) {
if (!vod) throw new Error('vod argument was missing');
if (!strapi) throw new Error('strapi argument was missing');
if (!uploadData) throw new Error('uploadData argument was missing');
const { cdnHostname } = zone;
const { strapiApiKey, strapiUrl } = strapi;
console.log(`🥤 lets create b2-file in Strapi`);
// Create the B2 file
const thumbResponse = await fetch(`${strapiUrl}/api/b2-files`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${strapiApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: {
key: uploadData.key,
uploadId: uploadData.uploadId,
url: uploadData.url,
cdnUrl: `https://${cdnHostname}/${uploadData.key}`
},
}),
});
if (!thumbResponse.ok) {
const msg = `🟠 Failed to create B2 file: ${thumbResponse.statusText}`
console.error(msg)
throw new Error(msg);
}
const thumbData = await thumbResponse.json() as IB2File;
console.log(`📀 B2 file creation complete for B2 file id: ${thumbData.data.id}`);
console.log(`🪇 lets associate B2-file with VOD ${vod.id} in Strapi`);
// Associate B2 file with VOD
const associateResponse = await fetch(`${strapiUrl}/api/vods/${vod.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${strapiApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: {
thumbnail: thumbData.data.id,
},
}),
});
if (!associateResponse.ok) {
const msg = `💀 Failed to associate B2 file with VOD: ${associateResponse.statusText}`;
console.error(msg)
throw new Error(msg)
}
console.log(`🫚 Association complete`);
const json = await associateResponse.json()
return json;
}
export async function main(vod: CVod, strapi: CStrapi, s3: S3, zone: CBunnyPullZone): CVod {
if (!vod) throw new Error('vod param is missing, and it is required.');
if (!strapi) throw new Error('strapi param is missing, and it is required.');
if (!s3) throw new Error(`s3 param is missing, and it is required.`);
if (!zone) throw new Error(`zone param is missing, and it is required.`);
const thumbnailFilePath = await __generateThumbnail(vod);
const b2Record = await uploadToB2(s3, thumbnailFilePath);
return associateB2WithVod(vod, strapi, b2Record, zone);
}

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
// Base Options recommended for all projects
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": false,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
// Enable strict type checking so you can catch bugs early
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Transpile our TypeScript code to JavaScript
"module": "NodeNext",
"outDir": "dist",
"lib": [
"es2022"
]
},
// Include the necessary files for your project
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

18
packages/boop/index.ts Normal file
View File

@ -0,0 +1,18 @@
import { bell } from 'taco'
import { IPagination } from 'types'
function main() {
const page: IPagination = {
page: 5,
pageCount: 20,
pageSize: 50,
total: 365
}
console.log(bell()+' '+page)
setTimeout(() => {
return main()
}, 2000)
}
main()

View File

@ -0,0 +1,22 @@
{
"name": "boop",
"type": "module",
"version": "1.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"build": "tsc --build"
},
"dependencies": {
"taco": "workspace:*",
"types": "workspace:*"
},
"keywords": [],
"author": "",
"license": "Unlicense",
"devDependencies": {
"typescript": "^5.5.3"
}
}

View File

@ -0,0 +1,31 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
taco:
specifier: workspace:*
version: link:../taco
types:
specifier: workspace:*
version: link:../types
devDependencies:
typescript:
specifier: ^5.5.3
version: 5.5.3
packages:
typescript@5.5.3:
resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
engines: {node: '>=14.17'}
hasBin: true
snapshots:
typescript@5.5.3: {}

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
// Base Options recommended for all projects
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
// Enable strict type checking so you can catch bugs early
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Transpile our TypeScript code to JavaScript
"module": "NodeNext",
"outDir": "dist",
"lib": [
"es2022",
"dom"
]
},
// Include the necessary files for your project
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@ -1 +0,0 @@
node_modules

View File

@ -1 +0,0 @@
use-node-version=>=20.0.0

View File

@ -1 +0,0 @@
lts/iron

View File

@ -1,19 +0,0 @@
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /app
RUN corepack enable
FROM base AS build
COPY ./packages/bot/package.json ./
COPY ./packages/bot/src ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install
FROM build AS dev
ENTRYPOINT ["pnpm"]
CMD ["run", "dev"]
FROM build AS run
ENTRYPOINT ["pnpm"]
CMD ["start"]

View File

@ -1,3 +1,26 @@
# bot
A.K.A. FutureButt, the discord bot that integrates into FP backend.
A.K.A. FutureButt, the discord bot that integrates into Futureporn backend.
## Features
* [ ] User submitted content (USC) notifications
* [ ] Embedded video
* [ ] Prevvy storyboard
* [ ] USC publishing
* [ ] USC rejection
## Usage
bot is a node.js app which uses ENV variables to ingest secrets. The following ENV vars are required.
```
DISCORD_TOKEN
DISCORD_CHANNEL_ID
```
Example invocation as follows.
DISCORD_TOKEN=your-token-goes-here DISCORD_CHANNEL_ID=1185024773231759481 node index.js

View File

@ -1,106 +0,0 @@
const { ButtonStyles, Client, ComponentTypes, ChannelTypes } = require("oceanic.js");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
}
});
client.on("ready", () => console.log("Ready as", client.user.tag));
client.on("messageCreate", async (msg) => {
if(msg.content.includes("!component")) {
await client.rest.channels.createMessage(msg.channelID, {
content: `Here's some buttons for you, ${msg.author.mention}.`,
components: [
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.TextButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY, // The style of button - full list: https://docs.oceanic.ws/latest/enums/Constants.ButtonStyles.html
customID: "some-string-you-will-see-later",
label: "Click!",
disabled: false, // If the button is disabled, false by default.
},
{
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY,
customID: "some-other-string",
label: "This Is Disabled",
disabled: true
},
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.URLButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.LINK,
label: "Open Link",
url: "https://docs.oceanic.ws"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.STRING_SELECT,
customID: "string-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
options: [
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectOption.html
{
default: true, // If this option is selected by default
description: "The description of the option", // Optional description
label: "Option One",
value: "value-1"
},
{
label: "Option Two",
value: "option-2"
}
],
placeholder: "Some Placeholder Text"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.CHANNEL_SELECT,
channelTypes: [ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_VOICE], // The types of channels that can be selected
customID: "channel-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
placeholder: "Some Placeholder Text"
}
]
}
]
});
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

View File

@ -1,94 +0,0 @@
const { Client } = require("oceanic.js");
const { readFileSync } = require("fs");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
}
});
client.on("ready", () => console.log("Ready as", client.user.tag));
client.on("messageCreate", async (msg) => {
if(msg.content.includes("!embed")) {
console.log(`'!embeds' was seen in chat!`)
console.log(msg)
await client.rest.channels.createMessage(msg.channelID, {
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedOptions.html
// Up to 10 in one message
embeds: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedAuthorOptions.html
author: {
name: "Author Name",
// An image url, or attachment://filename.ext
iconURL: "https://i.furry.cool/DonPride.png", // Optional
url: "https://docs.oceanic.ws" // Optional
},
// Array of https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedField.html
// Up to 25 in one message
fields: [
{
name: "Field One",
value: "Field One Value",
inline: true // If this field should be displayed inline (default: false)
},
{
name: "Field Two",
value: "Field Two Value",
inline: false
}
],
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedFooterOptions.html
footer: {
text: "Footer Text",
// An image url, or attachment://filename.ext
iconURL: "https://i.furry.cool/DonPride.png" // Optional
},
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedImageOptions.html
image: {
// An image url, or attachment://filename.ext
url: "https://i.furry.cool/DonPride.png"
},
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedThumbnailOptions.html
thumbnail: {
// An image url, or attachment://filename.ext
url: "https://i.furry.cool/DonPride.png"
},
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.EmbedOptions.html
color: 0xFFA500, // Base-10 color (0x prefix can be used for hex codes)
description: "My Cool Embed",
timestamp: new Date().toISOString(), // The current time - ISO 8601 format
title: "My Amazing Embed",
url: "https://docs.oceanic.ws"
}
]
});
} else if(msg.content.includes("!file")) {
await client.rest.channels.createMessage(msg.channelID, {
embeds: [
{
image: {
// This can also be used for author & footer images
url: "attachment://image.png"
}
}
],
files: [
{
name: "image.png",
contents: readFileSync(`${__dirname}/image.png`)
}
]
});
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

View File

@ -1,261 +0,0 @@
const { ButtonStyles, Client, ComponentTypes, ChannelTypes } = require("oceanic.js");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: ["GUILD_MESSAGES"] // If the message does not start with a mention to or somehow relate to your client, you will need the MESSAGE_CONTENT intent as well
}
});
client.on("ready", () => console.log("Ready as", client.user.tag));
client.on("messageCreate", async (msg) => {
console.log(msg.content)
if(msg.content.includes("!test")) {
await client.rest.channels.createMessage(msg.channelID, {
content: `HGERE IZ BUTTN'z 5 u, ${msg.author.mention}.`,
components: [
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.TextButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY, // The style of button - full list: https://docs.oceanic.ws/latest/enums/Constants.ButtonStyles.html
customID: "some-string-you-will-see-later",
label: "Click!",
disabled: false, // If the button is disabled, false by default.
},
{
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY,
customID: "some-other-string",
label: "This Is Disabled",
disabled: true
},
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.URLButton.html
type: ComponentTypes.BUTTON,
style: ButtonStyles.LINK,
label: "Open Link",
url: "https://docs.oceanic.ws"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.STRING_SELECT,
customID: "string-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
options: [
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectOption.html
{
default: true, // If this option is selected by default
description: "The description of the option", // Optional description
label: "Option One",
value: "value-1"
},
{
label: "Option Two",
value: "option-2"
}
],
placeholder: "Some Placeholder Text"
}
]
},
{
// The top level component must always be an action row.
// Full list of types: https://docs.oceanic.ws/latest/enums/Constants.ComponentTypes.html
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.MessageActionRow.html
type: ComponentTypes.ACTION_ROW,
components: [
{
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.SelectMenu.html
type: ComponentTypes.CHANNEL_SELECT,
channelTypes: [ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_VOICE], // The types of channels that can be selected
customID: "channel-select",
disabled: false,
maxValues: 1, // The maximum number of values that can be selected (default 1)
minValues: 1, // The minimum number of values that can be selected (default 1)
placeholder: "Some Placeholder Text"
}
]
}
]
});
}
});
client.on("interactionCreate", async(interaction) => {
console.log(`interaction!@`)
console.log(interaction)
switch(interaction.type) {
// https://docs.oceanic.ws/latest/classes/CommandInteraction.CommandInteraction.html
case InteractionTypes.APPLICATION_COMMAND: {
// defer interactions as soon as possible, you have three seconds to send any initial response
// if you wait too long, the interaction may be invalidated
await interaction.defer();
// If you want the response to be ephemeral, you can provide the flag to the defer function, like so:
// await interaction.defer(MessageFlags.EPHEMERAL);
// data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
switch(interaction.data.type) {
// Chat Input commands are what you use in the chat, i.e. slash commands
case ApplicationCommandTypes.CHAT_INPUT: {
if(interaction.data.name === "greet") {
// assume we have two options, user (called user) then string (called greeting) - first is required, second is not
// Get an option named `user` with the type USER - https://docs.oceanic.ws/dev/classes/InteractionOptionsWrapper.InteractionOptionsWrapper.html#getUser
// Setting the second parameter to true will throw an error if the option is not present
const user = interaction.data.options.getUser("user", true);
const greeting = interaction.data.options.getString("greeting", false) || "Hello, ";
// since we've already deferred the interaction, we cannot use createMessage (this is an initial response)
// we can only have one initial response, so we use createFollowup
await interaction.createFollowup({
content: `${greeting} ${user.mention}!`,
allowedMentions: {
users: [user.id]
}
});
}
// Chat Input application command interactions also have a set of resolved data, which is structured as so:
// https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionResolvedData.html
// the options wrapper pulls values out of resolved automatically, if you use the right method
break;
}
// User application commands are shown in the context menu when right-clicking on users
// `data` will have a target (and targetID) property with the user that the command was executed on
// These don't have options
case ApplicationCommandTypes.USER: {
if(interaction.data.name === "ping") {
await interaction.createFollowup({
content: `Pong! ${interaction.data.target.mention}`,
allowedMentions: {
users: [interaction.data.target.id]
}
});
}
break;
}
// Message application commands are shown in the context menu when right-clicking on messages
// `data` will have a target (and targetID) property with the message that the command was executed on
// Same as user commands, these don't have options
case ApplicationCommandTypes.MESSAGE: {
if(interaction.data.name === "author") {
await interaction.createFollowup({
content: `${interaction.data.target.author.mention} is the author of that message!`,
allowedMentions: {
users: [interaction.data.target.author.id]
}
});
}
break;
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ComponentInteraction.ComponentInteraction.html
case InteractionTypes.MESSAGE_COMPONENT: {
// same spiel as above
await interaction.defer();
// when you create a message with components, this will correspond with what you provided as the customID there
if(interaction.data.componentType === ComponentTypes.BUTTON) {
if(interaction.data.customID === "edit-message") {
// Edits the original message. This has an initial response variant: editParent
await interaction.editOriginal({
content: `This message was edited by ${interaction.user.mention}!`,
allowedMentions: {
users: [interaction.user.id]
}
});
} else if(interaction.data.customID === "my-amazing-button") {
await interaction.createFollowup({
content: "You clicked an amazing button!"
});
}
} else if(interaction.data.componentType === ComponentTypes.SELECT_MENU) {
// The `values` property under data contains all the selected values
await interaction.createFollowup({
content: `You selected: **${interaction.data.values.join("**, **")}**`
});
}
break;
}
// https://docs.oceanic.ws/latest/classes/AutocompleteInteraction.AutocompleteInteraction.html
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: {
// Autocomplete Interactions cannot be deferred
switch(interaction.data.name) {
case "test-autocomplete": {
// Autocomplete interactions data has a partial `options` property, which is the tree of options that are currently being filled in
// along with one at the end, which will have focused
// Setting the first parameter to true will throw an error if no focused option is present
const option = interaction.data.options.getFocused(true);
switch(option.name) {
case "test-option": {
return interaction.result([
{
name: "Choice 1",
nameLocalizations: {
"es-ES": "Opción 1"
},
value: "choice-1"
},
{
name: "Choice 2",
nameLocalizations: {
"es-ES": "Opción 2"
},
value: "choice-2"
}
]);
break;
}
}
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ModalSubmitInteraction.ModalSubmitInteraction.html
case InteractionTypes.MODAL_SUBMIT: {
// this will correspond with the customID you provided when creating the modal
switch(interaction.data.customID) {
case "test-modal": {
// the `components` property under data contains all the components that were submitted
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.ModalActionRow.html
console.log(interaction.data.components);
break;
}
}
break;
}
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

70
packages/bot/index.js Normal file
View File

@ -0,0 +1,70 @@
import 'dotenv/config';
import { Client, Events, GatewayIntentBits, Partials } from 'discord.js';
if (!process.env.DISCORD_TOKEN) throw new Error("DISCORD_TOKEN was missing from env");
if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env");
const channelId = ''+process.env.DISCORD_CHANNEL_ID
console.log(`channelId is ${channelId}`)
// Create a new client instance
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
],
partials: [
Partials.Message,
Partials.Channel,
Partials.Reaction,
]
});
// When the client is ready, run this code (only once).
// The distinction between `client: Client<boolean>` and `readyClient: Client<true>` is important for TypeScript developers.
// It makes some properties non-nullable.
client.once(Events.ClientReady, readyClient => {
console.log(`Ready! Logged in as ${readyClient.user.tag} yuhu`);
// client.channels.cache.get(process.env.DISCORD_CHANNEL_ID).send('testing 123');
readyClient.channels.fetch(channelId).then(channel => {
channel.send('generic welcome message!')
});
// console.log(readyClient.channels)
// const channel = readyClient.channels.cache.get(process.env.DISCORD_CHANNEL_ID);
// channel.send('testing 135');
});
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
if (commandName === 'react') {
const message = await interaction.reply({ content: 'You can react with Unicode emojis!', fetchReply: true });
message.react('😄');
}
})
client.on(Events.MessageReactionAdd, async (reaction, user) => {
// When a reaction is received, check if the structure is partial
if (reaction.partial) {
// If the message this reaction belongs to was removed, the fetching might result in an API error which should be handled
try {
await reaction.fetch();
} catch (error) {
console.error('Something went wrong when fetching the message:', error);
// Return as `reaction.message.author` may be undefined/null
return;
}
}
// Now the message has been cached and is fully available
console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`);
// The reaction is now also fully available and the properties will be reflected accurately:
console.log(`${reaction.count} user(s) have given the same reaction to this message!`);
});
// Log in to Discord with your client's token
client.login(process.env.DISCORD_TOKEN);

View File

@ -1,171 +0,0 @@
const { Client, InteractionTypes, MessageFlags, ComponentTypes, ApplicationCommandTypes } = require("oceanic.js");
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: {
intents: 0 // No intents are needed if you are only using interactions
}
});
client.on("ready", async() => {
console.log("Ready as", client.user.tag);
});
client.on("interactionCreate", async(interaction) => {
switch(interaction.type) {
// https://docs.oceanic.ws/latest/classes/CommandInteraction.CommandInteraction.html
case InteractionTypes.APPLICATION_COMMAND: {
// defer interactions as soon as possible, you have three seconds to send any initial response
// if you wait too long, the interaction may be invalidated
await interaction.defer();
// If you want the response to be ephemeral, you can provide the flag to the defer function, like so:
// await interaction.defer(MessageFlags.EPHEMERAL);
// data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
switch(interaction.data.type) {
// Chat Input commands are what you use in the chat, i.e. slash commands
case ApplicationCommandTypes.CHAT_INPUT: {
if(interaction.data.name === "greet") {
// assume we have two options, user (called user) then string (called greeting) - first is required, second is not
// Get an option named `user` with the type USER - https://docs.oceanic.ws/dev/classes/InteractionOptionsWrapper.InteractionOptionsWrapper.html#getUser
// Setting the second parameter to true will throw an error if the option is not present
const user = interaction.data.options.getUser("user", true);
const greeting = interaction.data.options.getString("greeting", false) || "Hello, ";
// since we've already deferred the interaction, we cannot use createMessage (this is an initial response)
// we can only have one initial response, so we use createFollowup
await interaction.createFollowup({
content: `${greeting} ${user.mention}!`,
allowedMentions: {
users: [user.id]
}
});
}
// Chat Input application command interactions also have a set of resolved data, which is structured as so:
// https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionResolvedData.html
// the options wrapper pulls values out of resolved automatically, if you use the right method
break;
}
// User application commands are shown in the context menu when right-clicking on users
// `data` will have a target (and targetID) property with the user that the command was executed on
// These don't have options
case ApplicationCommandTypes.USER: {
if(interaction.data.name === "ping") {
await interaction.createFollowup({
content: `Pong! ${interaction.data.target.mention}`,
allowedMentions: {
users: [interaction.data.target.id]
}
});
}
break;
}
// Message application commands are shown in the context menu when right-clicking on messages
// `data` will have a target (and targetID) property with the message that the command was executed on
// Same as user commands, these don't have options
case ApplicationCommandTypes.MESSAGE: {
if(interaction.data.name === "author") {
await interaction.createFollowup({
content: `${interaction.data.target.author.mention} is the author of that message!`,
allowedMentions: {
users: [interaction.data.target.author.id]
}
});
}
break;
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ComponentInteraction.ComponentInteraction.html
case InteractionTypes.MESSAGE_COMPONENT: {
// same spiel as above
await interaction.defer();
// when you create a message with components, this will correspond with what you provided as the customID there
if(interaction.data.componentType === ComponentTypes.BUTTON) {
if(interaction.data.customID === "edit-message") {
// Edits the original message. This has an initial response variant: editParent
await interaction.editOriginal({
content: `This message was edited by ${interaction.user.mention}!`,
allowedMentions: {
users: [interaction.user.id]
}
});
} else if(interaction.data.customID === "my-amazing-button") {
await interaction.createFollowup({
content: "You clicked an amazing button!"
});
}
} else if(interaction.data.componentType === ComponentTypes.SELECT_MENU) {
// The `values` property under data contains all the selected values
await interaction.createFollowup({
content: `You selected: **${interaction.data.values.join("**, **")}**`
});
}
break;
}
// https://docs.oceanic.ws/latest/classes/AutocompleteInteraction.AutocompleteInteraction.html
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: {
// Autocomplete Interactions cannot be deferred
switch(interaction.data.name) {
case "test-autocomplete": {
// Autocomplete interactions data has a partial `options` property, which is the tree of options that are currently being filled in
// along with one at the end, which will have focused
// Setting the first parameter to true will throw an error if no focused option is present
const option = interaction.data.options.getFocused(true);
switch(option.name) {
case "test-option": {
return interaction.result([
{
name: "Choice 1",
nameLocalizations: {
"es-ES": "Opción 1"
},
value: "choice-1"
},
{
name: "Choice 2",
nameLocalizations: {
"es-ES": "Opción 2"
},
value: "choice-2"
}
]);
break;
}
}
}
}
break;
}
// https://docs.oceanic.ws/latest/classes/ModalSubmitInteraction.ModalSubmitInteraction.html
case InteractionTypes.MODAL_SUBMIT: {
// this will correspond with the customID you provided when creating the modal
switch(interaction.data.customID) {
case "test-modal": {
// the `components` property under data contains all the components that were submitted
// https://docs.oceanic.ws/latest/interfaces/Types_Channels.ModalActionRow.html
console.log(interaction.data.components);
break;
}
}
break;
}
}
});
// An error handler
client.on("error", (error) => {
console.error("Something went wrong:", error);
});
// Connect to Discord
client.connect();

View File

@ -1,22 +1,20 @@
{
"name": "fp-bot",
"name": "bot",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"main": "index.js",
"scripts": {
"dev": "node --import=tsx --watch ./src/index.ts"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index",
"dev": "nodemon index"
},
"keywords": [],
"author": "",
"license": "CC0-1.0",
"devDependencies": {
"tsx": "^4.7.2"
},
"license": "Unlicense",
"dependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.12.6",
"discordeno": "^18.0.1",
"express": "^4.19.2",
"oceanic.js": "^1.10.0"
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
"nodemon": "^3.1.4"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
import { getBotIdFromToken, Intents } from 'discordeno';
/** The bot id, derived from the bot token. */
export const BOT_ID = getBotIdFromToken(process.env.DISCORD_TOKEN as string);
export const EVENT_HANDLER_URL = `http://${process.env.EVENT_HANDLER_HOST}:${process.env.EVENT_HANDLER_PORT}`;
export const REST_URL = `http://${process.env.REST_HOST}:${process.env.REST_PORT}`;
export const GATEWAY_URL = `http://${process.env.GATEWAY_HOST}:${process.env.GATEWAY_PORT}`;
// Gateway Proxy Configurations
/** The gateway intents you would like to use. */
export const INTENTS: Intents =
// SETUP-DD-TEMP: Add the intents you want enabled here. Or Delete the intents you don't want in your bot.
Intents.DirectMessageReactions |
Intents.DirectMessageTyping |
Intents.DirectMessages |
Intents.GuildBans |
Intents.GuildEmojis |
Intents.GuildIntegrations |
Intents.GuildInvites |
Intents.GuildMembers |
Intents.GuildMessageReactions |
Intents.GuildMessageTyping |
Intents.GuildMessages |
Intents.GuildPresences |
Intents.GuildVoiceStates |
Intents.GuildWebhooks |
Intents.Guilds;

View File

@ -1,58 +0,0 @@
import { BASE_URL, createRestManager } from 'discordeno';
import express, { Request, Response } from 'express';
// import { setupAnalyticsHooks } from '../analytics.js';
import { REST_URL } from './configs.js';
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
const REST_PORT = process.env.REST_PORT as string;
const rest = createRestManager({
token: DISCORD_TOKEN,
secretKey: REST_AUTHORIZATION,
customUrl: REST_URL,
debug: console.log,
});
// Add send fetching analytics hook to rest
// setupAnalyticsHooks(rest);
// @ts-expect-error
rest.convertRestError = (errorStack, data) => {
if (!data) return { message: errorStack.message };
return { ...data, message: errorStack.message };
};
const app = express();
app.use(
express.urlencoded({
extended: true,
}),
);
app.use(express.json());
app.all('/*', async (req: Request, res: Response) => {
if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) {
return res.status(401).json({ error: 'Invalid authorization key.' });
}
try {
const result = await rest.runMethod(rest, req.method, `${BASE_URL}${req.url}`, req.body);
if (result) {
res.status(200).json(result);
} else {
res.status(204).json();
}
} catch (error: any) {
console.log(error);
res.status(500).json(error);
}
});
app.listen(REST_PORT, () => {
console.log(`REST listening at ${REST_URL}`);
});

View File

@ -1,9 +0,0 @@
# REST Proxy
This folder will contain the code for our REST proxy. This is going to become the single source that all of our bot will
use to communciate to the Discord API.
## Further Steps
- Express framework to create the listener however, you can replace it with anything you like. Express is quite a
bloated framework. Feel free to optimize to a better framework.

View File

@ -1,30 +0,0 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"importHelpers": true,
"allowUnusedLabels": false,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"stripInternal": true,
"noFallthroughCasesInSwitch": true,
"useUnknownInCatchVariables": false,
"allowUnreachableCode": false,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["./src/**/*", ".env"],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node",
"swc": true
}
}

View File

@ -0,0 +1,3 @@
{
"loader": "ts-node/esm"
}

View File

@ -1,5 +1,5 @@
import { getProminentColor, rgbToHex, getStoryboard } from './image.js'
import { getProminentColor, rgbToHex, getStoryboard } from './index.js'
import { expect } from 'chai'
import { describe } from 'mocha'
import path from 'node:path'
@ -14,9 +14,9 @@ describe('image', function () {
})
describe('rgbToHex', function () {
it('should convert color to hex {String} hexidecimal code', function () {
const mulberry = [255, 87, 51]
const screaminGreen = [77, 255, 106]
const amaranth = [227, 64, 81]
const mulberry = [255, 87, 51] as const
const screaminGreen = [77, 255, 106] as const
const amaranth = [227, 64, 81] as const
expect(rgbToHex(...mulberry)).to.equal('#ff5733')
expect(rgbToHex(...screaminGreen)).to.equal('#4dff6a')
expect(rgbToHex(...amaranth)).to.equal('#e34051')

30
packages/image/index.ts Normal file
View File

@ -0,0 +1,30 @@
// import ColorThief from 'colorthief';
import sharp from 'sharp';
import Prevvy from 'prevvy';
import path from 'path';
import { getTmpFile } from 'utils';
export async function getProminentColor(imageFile: string): Promise<string> {
const { dominant } = await sharp(imageFile).stats();
const { r, g, b } = dominant;
return rgbToHex(r, g, b);
}
export function rgbToHex(r: number, g: number, b: number): string {
return "#" + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1);
}
export async function getStoryboard(imageFileOrUrl: string): Promise<string> {
let base = path.basename(imageFileOrUrl);
let outputImagePath = getTmpFile(base);
let options = {
input: imageFileOrUrl,
output: outputImagePath,
width: 265,
cols: 5,
rows: 5,
};
let prevvy = new Prevvy(options);
await prevvy.generate();
return outputImagePath;
}

View File

@ -0,0 +1,26 @@
{
"name": "image",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "Unlicense",
"dependencies": {
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.7",
"prevvy": "^7.0.1",
"sharp": "^0.33.4",
"utils": "workspace:^"
},
"devDependencies": {
"chai": "^5.1.1",
"mocha": "^10.6.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Base Options recommended for all projects
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
// Enable strict type checking so you can catch bugs early
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Transpile our TypeScript code to JavaScript
"module": "NodeNext",
"outDir": "dist",
"lib": [
"es2022"
]
},
// Include the necessary files for your project
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,2 +1,3 @@
# next
HTML5 frontend for https://futureporn.net

View File

@ -13,7 +13,7 @@ export default async function Page() {
<section className="hero">
<div className="hero-body">
<p className="title">About</p>
<p>It's the worst feeling when a VOD disappears from the internet. It means you missed out, it's gone, and you may never experience what your peers got to take part in.</p>
<p>It&apos;s the worst feeling when a VOD disappears from the internet. It means you missed out, it&apos;s gone, and you may never experience what your peers got to take part in.</p>
<p>Futureporn is created by fans, for fans. Missed a stream? We got you, bro.</p>
<p>Together we can end 404s and create an everlasting archive of lewdtuber livestreams.</p>

View File

@ -1,7 +1,7 @@
import { getVodTitle } from '@/components/vod-page';
import { getUrl, getAllVods } from "@/lib/vods"
import { IVod } from "@/lib/vods"
import { getVodTitle } from '@/app/components/vod-page';
import { getUrl, getAllVods } from "@/app/lib/vods"
import { IVod } from "@/app/lib/vods"
/*

View File

@ -1,6 +1,6 @@
import StreamPage from '@/components/stream-page';
import { getStreamByCuid } from '@/lib/streams';
import StreamPage from '@/app/components/stream-page';
import { getStreamByCuid } from '@/app/lib/streams';
interface IPageParams {

View File

@ -1,8 +1,8 @@
import Pager from "@/components/pager";
import StreamsList from "@/components/streams-list";
import StreamsTable from '@/components/streams-table';
import { getAllStreams, getStreamsForVtuber } from "@/lib/streams";
// import { getAllVtubers } from "@/lib/vtubers";
import Pager from "@/app/components/pager";
import StreamsList from "@/app/components/streams-list";
import StreamsTable from '@/app/components/streams-table';
import { getAllStreams, getStreamsForVtuber } from "@/app/lib/streams";
// import { getAllVtubers } from "@/app/lib/vtubers";
import { notFound } from "next/navigation";

View File

@ -1,6 +1,6 @@
import { getAllStreamsForVtuber, getStreamCountForVtuber } from "@/lib/streams";
import { getVodsForVtuber } from "@/lib/vods";
import { IVtuber } from "@/lib/vtubers";
import { getAllStreamsForVtuber, getStreamCountForVtuber } from "@/app/lib/streams";
import { getVodsForVtuber } from "@/app/lib/vods";
import { IVtuber } from "types";
export interface IArchiveProgressProps {
vtuber: IVtuber;

View File

@ -7,7 +7,7 @@ import { faPatreon } from '@fortawesome/free-brands-svg-icons';
import { useLocalStorageValue } from '@react-hookz/web';
import { faRightFromBracket } from '@fortawesome/free-solid-svg-icons';
import Skeleton from 'react-loading-skeleton';
import { strapiUrl } from '@/lib/constants';
import { strapiUrl } from '@/app/lib/constants';
// import NextAuth from 'next-auth'; // this is (pipedream) wishlist
// import Providers from 'next-auth/providers';

View File

@ -1,6 +1,6 @@
import { getCampaign } from "@/lib/patreon";
import { getGoals, IGoals } from '@/lib/pm'
import { getCampaign } from "@/app/lib/patreon";
import { getGoals, IGoals } from '@/app/lib/pm'
import Image from "next/legacy/image";
import React from 'react';
import Link from 'next/link'

View File

@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { faUser, faUpload } from "@fortawesome/free-solid-svg-icons";
import Link from 'next/link'
import { LoginButton, useAuth } from '@/components/auth'
import { LoginButton, useAuth } from '@/app/components/auth'
export default function Navbar() {

View File

@ -1,4 +1,4 @@
import { IStream } from "@/lib/streams";
import { IStream } from "types";
import Link from "next/link"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCalendar } from "@fortawesome/free-solid-svg-icons";

View File

@ -1,8 +1,8 @@
'use client';
import { IStream } from "@/lib/streams";
import { IStream } from "types";
// import NotFound from "app/streams/[cuid]/not-found";
import { IVod } from "@/lib/vods";
import { IVod } from "@/app/lib/vods";
import Link from "next/link";
import Image from "next/legacy/image";
import { LocalizedDate } from "./localized-date";
@ -37,7 +37,7 @@ function determineStatus(stream: IStream): Status {
if (stream.attributes.vods.data.length < 1) {
return 'missing'
} else {
if (stream.attributes.vods.data.some(vod => !hasNote(vod))) {
if (stream.attributes.vods.data.some((vod: IVod) => !hasNote(vod))) {
return 'good';
} else {
return 'issue';

View File

@ -1,9 +1,9 @@
import { IStream } from "@/lib/streams";
import NotFound from "app/vt/[slug]/not-found";
import { IStream } from "types";
import NotFound from "@/app/vt/[slug]/not-found";
import { LocalizedDate } from "./localized-date";
import Link from "next/link";
import ChaturbateIcon from "@/components/icons/chaturbate";
import FanslyIcon from "@/components/icons/fansly";
import ChaturbateIcon from "@/app/components/icons/chaturbate";
import FanslyIcon from "@/app/components/icons/fansly";
import Image from "next/legacy/image";
export interface IStreamProps {

View File

@ -1,9 +1,10 @@
import React from 'react'
import Link from 'next/link';
import { IVtuber } from '@/lib/vtubers';
import { IVtuber } from 'types';
import { notFound } from 'next/navigation';
import { IStream, getAllStreams } from '@/lib/streams';
import { StreamSummary } from '@/components/stream';
import { getAllStreams } from '@/app/lib/streams';
import { IStream } from 'types';
import { StreamSummary } from '@/app/components/stream';
interface IStreamsListProps {
vtubers: IVtuber[];

View File

@ -19,7 +19,8 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { fetchStreamData, IStream } from '@/lib/streams'
import { fetchStreamData } from '@/app/lib/streams'
import { IStream } from 'types'
const queryClient = new QueryClient()

View File

@ -1,13 +1,13 @@
'use client';
import { ITagVodRelation, ITagVodRelationsResponse } from "@/lib/tag-vod-relations"
import { ITagVodRelation, ITagVodRelationsResponse } from "@/app/lib/tag-vod-relations"
import { isWithinInterval, subHours } from "date-fns";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AuthContext, IUseAuth } from "./auth";
import { useContext, useEffect, useState } from "react";
import { useRouter } from 'next/navigation';
import { strapiUrl } from "@/lib/constants";
import { strapiUrl } from "@/app/lib/constants";
export interface ITagParams {
tvr: ITagVodRelation;

View File

@ -1,18 +1,18 @@
'use client';
import { useState, useCallback, useEffect, useContext } from 'react';
import { IVod } from '@/lib/vods';
import { IVod } from '@/app/lib/vods';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faX, faTags } from "@fortawesome/free-solid-svg-icons";
import { formatTimestamp } from '@/lib/dates';
import { readOrCreateTagVodRelation } from '@/lib/tag-vod-relations';
import { readOrCreateTag } from '@/lib/tags';
import { formatTimestamp } from '@/app/lib/dates';
import { readOrCreateTagVodRelation } from '@/app/lib/tag-vod-relations';
import { readOrCreateTag } from '@/app/lib/tags';
import { useAuth } from './auth';
import { debounce } from 'lodash';
import { strapiUrl } from '@/lib/constants';
import { strapiUrl } from '@/app/lib/constants';
import { VideoContext } from './video-context';
import { useForm } from "react-hook-form";
import { ITimestamp, createTimestamp } from '@/lib/timestamps';
import { ITimestamp, createTimestamp } from '@/app/lib/timestamps';
import { useRouter } from 'next/navigation';
import styles from '@/assets/styles/fp.module.css'
import qs from 'qs';

View File

@ -1,13 +1,13 @@
import React, { useContext, useState, useEffect } from "react";
import { IVod } from "@/lib/vods";
import { IVod } from "@/app/lib/vods";
import {
ITimestamp,
deleteTimestamp
} from "@/lib/timestamps";
} from "@/app/lib/timestamps";
import {
formatTimestamp,
formatUrlTimestamp,
} from "@/lib/dates";
} from "@/app/lib/dates";
import Link from 'next/link';
import { faClock, faLink, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

View File

@ -1,6 +1,6 @@
import React from 'react';
import { IToy, IToysResponse } from '@/lib/toys';
import { IVtuber } from '@/lib/vtubers';
import { IToy, IToysResponse } from '@/app/lib/toys';
import { IVtuber } from 'types';
import Link from 'next/link';
import Image from "next/legacy/image";

View File

@ -1,24 +1,28 @@
'use client';
import { IVtuber } from "@/lib/vtubers";
import { IStream } from "@/lib/streams";
import { IVtuber } from "types";
import { useSearchParams } from 'next/navigation';
import React, { useContext, useState, useEffect } from 'react';
import { UppyContext } from 'app/uppy';
import { LoginButton, useAuth } from '@/components/auth';
import React from 'react';
import AwsS3 from '@uppy/aws-s3';
import RemoteSources from '@uppy/remote-sources';
import { LoginButton, useAuth } from '@/app/components/auth';
import { Dashboard } from '@uppy/react';
import styles from '@/assets/styles/fp.module.css'
import { projektMelodyEpoch } from "@/lib/constants";
import { projektMelodyEpoch } from "@/app/lib/constants";
import add from "date-fns/add";
import sub from "date-fns/sub";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle, faEraser, faPaperPlane, faSpinner, faX, faXmark } from "@fortawesome/free-solid-svg-icons";
import { useForm, useFieldArray, ValidationMode } from 'react-hook-form';
import { faEraser, faPaperPlane, faSpinner, faX, faXmark } from "@fortawesome/free-solid-svg-icons";
import { useForm, ValidationMode } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
import qs from 'qs';
import { toast } from "react-toastify";
import { ErrorMessage } from "@hookform/error-message"
import Uppy from '@uppy/core';
import { companionUrl } from '@/app/lib/constants';
interface IUploadFormProps {
vtubers: IVtuber[];
@ -58,9 +62,38 @@ const validationSchema = Yup.object().shape({
export default function UploadForm({ vtubers }: IUploadFormProps) {
const searchParams = useSearchParams();
const cuid = searchParams.get('cuid');
const uppy = useContext(UppyContext);
const { authData } = useAuth();
const uppy = new Uppy(
{
autoProceed: true,
debug: true,
logger: {
debug: console.info,
warn: console.log,
error: console.error
},
}
)
.use(RemoteSources, {
companionUrl,
sources: [
'GoogleDrive',
'Dropbox',
'Url'
]
})
.use(AwsS3, {
companionUrl,
shouldUseMultipart: true,
abortMultipartUpload: () => {}, // @see https://github.com/transloadit/uppy/issues/1197#issuecomment-491756118
companionHeaders: {
'authorization': `Bearer ${authData?.accessToken}`
}
})
const formOptions = {
resolver: yupResolver(validationSchema),
@ -159,23 +192,24 @@ export default function UploadForm({ vtubers }: IUploadFormProps) {
uppy.on('complete', async (result: any) => {
console.log('uppy complete! ')
console.log(result)
for (const s of result.successful) {
if (!s?.s3Multipart) {
const m = 'file was missing s3Multipart'
toast.error(`${m}`, { theme: 'dark' });
setError('root.serverError', {
type: 'remote',
message: 'file was missing s3Multipart'
message: m
})
// throw new Error('file was missing s3Multipart')
throw new Error(m)
}
}
console.log('uppy complete! ')
console.log(result)
toast.success(`upload complete`);
let files = result.successful.map((f: any) => ({ key: f.s3Multipart.key, uploadId: f.s3Multipart.uploadId }));
setValue('files', files);
});
return (
<>
@ -215,19 +249,30 @@ export default function UploadForm({ vtubers }: IUploadFormProps) {
uppy={uppy}
theme='dark'
proudlyDisplayPoweredByUppy={true}
showProgressDetails={true}
/>
{/* This form is hidden. Why? */}
{/*
Here is how we upload the files to the server.
From uppy, we get a list of files.
we add the files to a hidden input box.
the input box is part of the form which gets POSTed.
*/}
<input
required
hidden={false}
hidden={true}
style={{ display: 'block' }}
className="input" type="text"
{...register('files')}
></input>
<button className="button" onClick={() => { setValue('files', [
{
"key": "4b4063a2-6b57-48f1-8565-a12ddce473e9-E1tB0KoUcAYJTni.jpg",
"uploadId": "4_z7d53875ff1c32a1983d30b18_f2129582707239923_d20240708_m003328_c000_v0001086_t0006_u01720398808368"
}
]); }}>(Debug) Add a list of files</button>
{errors.files && <p className="help is-danger">{errors.files.message?.toString()}</p>}
@ -363,7 +408,7 @@ export default function UploadForm({ vtubers }: IUploadFormProps) {
)}
{isSubmitSuccessful && (
<>
<aside className="notification mt-5 is-success">Thank you for uploading! </aside>
<aside className="notification mt-5 is-success">Thank you for uploading! A moderator will review the VOD before being published.</aside>
<button onClick={() => {
reset(); // reset form
const files = uppy.getFiles()

View File

@ -1,7 +1,6 @@
import VideoApiElement from "@mux/mux-player/dist/types/video-api";
import { MutableRefObject, createContext, useState } from "react";
import { ITagVodRelation } from "@/lib/tag-vod-relations";
import { createContext } from "react";
import { ITagVodRelation } from "@/app/lib/tag-vod-relations";
export interface IVideoContextValue {
timeStamp: number;

View File

@ -1,17 +1,17 @@
'use client';
import { IVod } from "@/lib/vods";
import { IVod } from "@/app/lib/vods";
import { useRef, useState, useEffect, useCallback } from "react";
import { VideoPlayer } from "./video-player";
import { Tagger } from './tagger';
import { ITimestamp, getTimestampsForVod } from "@/lib/timestamps";
import { ITimestamp, getTimestampsForVod } from "@/app/lib/timestamps";
import { TimestampsList } from "./timestamps-list";
import { ITagVodRelation } from "@/lib/tag-vod-relations";
import { ITagVodRelation } from "@/app/lib/tag-vod-relations";
import { VideoContext } from "./video-context";
import { getVodTitle } from "./vod-page";
import { useSearchParams } from 'next/navigation';
import VideoApiElement from "@mux/mux-player/dist/types/video-api";
import { parseUrlTimestamp } from "@/lib/dates";
import type VideoApiElement from "@mux/mux-player";
import { parseUrlTimestamp } from "@/app/lib/dates";
import { faTags, faNoteSticky, faClock } from "@fortawesome/free-solid-svg-icons";
import { Tag } from './tag';
import VodNav from './vod-nav';

View File

@ -1,16 +1,16 @@
'use client';
import { useEffect, useState, forwardRef, useContext, Ref } from 'react';
import { IVod } from '@/lib/vods';
import { useAuth } from '@/components/auth';
import { IVod } from '@/app/lib/vods';
import { useAuth } from '@/app/components/auth';
import { getVodTitle } from './vod-page';
import { VideoSourceSelector } from '@/components/video-source-selector'
import { buildIpfsUrl } from '@/lib/ipfs';
import { strapiUrl } from '@/lib/constants';
import { VideoSourceSelector } from '@/app/components/video-source-selector'
import { buildIpfsUrl } from '@/app/lib/ipfs';
import { strapiUrl } from '@/app/lib/constants';
import MuxPlayer from '@mux/mux-player-react/lazy';
import { VideoContext } from './video-context';
import MuxPlayerElement from '@mux/mux-player';
import VideoApiElement from "@mux/mux-player/dist/types/video-api";
import type VideoApiElement from "@mux/mux-player";
interface IPlayerProps {
vod: IVod;

View File

@ -1,14 +1,8 @@
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPatreon } from "@fortawesome/free-brands-svg-icons";
import { faVideo } from "@fortawesome/free-solid-svg-icons";
import { getSafeDate, getDateFromSafeDate } from '@/lib/dates';
import { IVtuber } from '@/lib/vtubers';
import { getSafeDate } from '@/app/lib/dates';
import { IVtuber } from 'types';
import Image from "next/legacy/image"
import { LocalizedDate } from '@/components/localized-date'
import { IMuxAsset, IMuxAssetResponse } from "@/lib/types";
import { IB2File } from "@/lib/b2File";
import VtuberButton from "./vtuber-button";
import { LocalizedDate } from '@/app/components/localized-date'
interface IVodCardProps {
id: number;

View File

@ -5,10 +5,10 @@ import { faXTwitter } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Image from "next/legacy/image";
import Link from 'next/link';
import { IVod } from '@/lib/vods';
import { buildIpfsUrl } from '@/lib/ipfs';
import { getSafeDate } from "@/lib/dates";
import { StreamButton } from '@/components/stream-button';
import { IVod } from '@/app/lib/vods';
import { buildIpfsUrl } from '@/app/lib/ipfs';
import { getSafeDate } from "@/app/lib/dates";
import { StreamButton } from '@/app/components/stream-button';
import VtuberButton from "./vtuber-button";
import { LocalizedDate } from "./localized-date";

View File

@ -1,5 +1,5 @@
import { getUrl, getNextVod, getPreviousVod, getLocalizedDate } from '@/lib/vods';
import { IVod } from '@/lib/vods';
import { getUrl, getNextVod, getPreviousVod, getLocalizedDate } from '@/app/lib/vods';
import { IVod } from '@/app/lib/vods';
import Link from 'next/link';
import { VideoInteractive } from './video-interactive';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@ -1,8 +1,8 @@
import React from 'react'
import Link from 'next/link';
import VodCard from './vod-card';
import { IVtuber, IVtuberResponse } from '@/lib/vtubers';
import { IVodsResponse, IVod } from '@/lib/vods';
import { IVtuber } from 'types';
import { IVod } from '@/app/lib/vods';
import { getVodTitle } from './vod-page';
import { notFound } from 'next/navigation';

View File

@ -1,8 +1,8 @@
import Link from "next/link";
import type { IVtuber } from '@/lib/vtubers';
import { getVodsForVtuber } from "@/lib/vods";
import type { IVtuber } from 'types';
import { getVodsForVtuber } from "@/app/lib/vods";
import Image from "next/legacy/image"
import NotFound from "app/vt/[slug]/not-found";
import NotFound from "@/app/vt/[slug]/not-found";
import ArchiveProgress from "./archive-progress";
export default async function VTuberCard(vtuber: IVtuber) {

View File

@ -3,9 +3,9 @@
import { useSearchParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { strapiUrl } from '@/lib/constants'
import { useAuth, IAuthData, IUser, IJWT } from '@/components/auth'
import { DangerNotification } from '@/components/notifications'
import { strapiUrl } from '@/app/lib/constants'
import { useAuth, IAuthData, IUser, IJWT } from '@/app/components/auth'
import { DangerNotification } from '@/app/components/notifications'
export type AccessToken = string | null;
@ -83,7 +83,7 @@ export default function Page() {
useEffect(() => {
initAuth()
}, [])
})

View File

@ -3,8 +3,8 @@ import { getVtuberBySlug } from '../lib/vtubers'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { faLink } from '@fortawesome/free-solid-svg-icons';
import { projektMelodyEpoch } from '@/lib/constants';
import LinkableHeading from '@/components/linkable-heading';
import { projektMelodyEpoch } from '@/app/lib/constants';
import LinkableHeading from '@/app/components/linkable-heading';
export default async function Page() {
return (

View File

@ -1,4 +1,4 @@
import { generateFeeds } from "@/lib/rss"
import { generateFeeds } from "@/app/lib/rss"
export async function GET() {
const feeds = await generateFeeds()

View File

@ -1,4 +1,4 @@
import { generateFeeds } from "@/lib/rss"
import { generateFeeds } from "@/app/lib/rss"
export async function GET() {
const { atom1 } = await generateFeeds()

View File

@ -1,4 +1,4 @@
import { generateFeeds } from "@/lib/rss"
import { generateFeeds } from "@/app/lib/rss"
export async function GET() {
const { rss2 } = await generateFeeds()

View File

@ -1,5 +1,5 @@
import { getGoals } from "@/lib/pm";
import { getCampaign } from "@/lib/patreon";
import { getGoals } from "@/app/lib/pm";
import { getCampaign } from "@/app/lib/patreon";
interface IFundingStatusBadgeProps {
completedPercentage: number;

View File

@ -1,6 +1,6 @@
import VodsList from '@/components/vods-list';
import { getVods } from '@/lib/vods';
import Pager from '@/components/pager';
import VodsList from '@/app/components/vods-list';
import { getVods } from '@/app/lib/vods';
import Pager from '@/app/components/pager';
interface IPageParams {
params: {

View File

@ -1,8 +1,8 @@
import VodsList from '@/components/vods-list';
import { IVodsResponse } from '@/lib/vods';
import Pager from '@/components/pager';
import { getVods } from '@/lib/vods';
import VodsList from '@/app/components/vods-list';
import { IVodsResponse } from '@/app/lib/vods';
import Pager from '@/app/components/pager';
import { getVods } from '@/app/lib/vods';
interface IPageParams {

View File

@ -1,4 +1,4 @@
import { IMeta } from "./types";
import { IMeta } from "types";
export interface IB2File {
id: number;

View File

@ -1,5 +1,5 @@
import { strapiUrl, patreonVideoAccessBenefitId, giteaUrl } from './constants'
import { IAuthData } from '@/components/auth';
import { IAuthData } from '@/app/components/auth';
export interface IPatron {
username: string;

View File

@ -1,7 +1,7 @@
import { authorName, authorEmail, siteUrl, title, description, siteImage, favicon, authorLink } from './constants'
import { Feed } from "feed";
import { getVods, getUrl, IVod } from '@/lib/vods'
import { ITagVodRelation } from '@/lib/tag-vod-relations';
import { getVods, getUrl, IVod } from '@/app/lib/vods'
import { ITagVodRelation } from '@/app/lib/tag-vod-relations';
export async function generateFeeds() {
const feedOptions = {

View File

@ -1,38 +1,11 @@
import { siteUrl, strapiUrl } from './constants';
import { getSafeDate } from './dates';
import { IVodsResponse } from './vods';
import { IVtuber, IVtuberResponse } from './vtubers';
import { ITweetResponse } from './tweets';
import { IMeta } from './types';
import qs from 'qs';
import { IStream } from 'types';
import { IStreamsResponse } from 'types';
export interface IStream {
id: number;
attributes: {
date: string;
date2: string;
archiveStatus: 'good' | 'issue' | 'missing';
vods: IVodsResponse;
cuid: string;
vtuber: IVtuberResponse;
tweet: ITweetResponse;
isChaturbateStream: boolean;
isFanslyStream: boolean;
}
}
export interface IStreamResponse {
data: IStream;
meta: IMeta;
}
export interface IStreamsResponse {
data: IStream[];
meta: IMeta;
}
const fetchStreamsOptions = {
next: {

View File

@ -10,8 +10,8 @@ import qs from 'qs';
import { strapiUrl } from './constants'
import { ITagResponse, IToyTagResponse } from './tags';
import { IVod, IVodResponse } from './vods';
import { IAuthData } from '@/components/auth';
import { IMeta } from './types';
import { IAuthData } from '@/app/components/auth';
import { IMeta } from 'types';
export interface ITagVodRelation {
id: number;

View File

@ -3,9 +3,9 @@ import { fetchPaginatedData } from './fetchers';
import { IVod } from './vods';
import slugify from 'slugify';
import { IToy } from './toys';
import { IAuthData } from '@/components/auth';
import { IAuthData } from '@/app/components/auth';
import qs from 'qs';
import { IMeta } from './types';
import { IMeta } from 'types';
export interface ITag {

View File

@ -2,9 +2,9 @@
import qs from 'qs';
import { strapiUrl } from './constants'
import { IAuthData } from '@/components/auth';
import { IAuthData } from '@/app/components/auth';
import { ITagsResponse, ITag, ITagResponse } from './tags';
import { IMeta } from './types';
import { IMeta } from 'types';
export interface ITimestamp {
id: number;

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