add file_size and recording_state to db
ci / build (push) Has been cancelled
Details
ci / build (push) Has been cancelled
Details
This commit is contained in:
parent
8faa5e2195
commit
5bb2296a00
|
@ -60,3 +60,5 @@ https://uppy.fp.sbtp.xyz/metrics
|
||||||
### Code is run more than it is read
|
### Code is run more than it is read
|
||||||
|
|
||||||
### The computer doesn't care
|
### The computer doesn't care
|
||||||
|
|
||||||
|
### [ONE SHOT. ONE LIFE](https://www.youtube.com/watch?v=Rh-ohspuCmE)
|
1
Tiltfile
1
Tiltfile
|
@ -145,6 +145,7 @@ docker_build(
|
||||||
'./pnpm-lock.yaml',
|
'./pnpm-lock.yaml',
|
||||||
'./pnpm-workspace.yaml',
|
'./pnpm-workspace.yaml',
|
||||||
'./services/bot',
|
'./services/bot',
|
||||||
|
'./packages/types',
|
||||||
],
|
],
|
||||||
dockerfile='./d.bot.dockerfile',
|
dockerfile='./d.bot.dockerfile',
|
||||||
target='dev',
|
target='dev',
|
||||||
|
|
|
@ -20,6 +20,8 @@ spec:
|
||||||
- name: bot
|
- name: bot
|
||||||
image: "{{ .Values.bot.imageName }}"
|
image: "{{ .Values.bot.imageName }}"
|
||||||
env:
|
env:
|
||||||
|
- name: POSTGREST_URL
|
||||||
|
value: "{{ .Values.postgrest.url }}"
|
||||||
- name: AUTOMATION_USER_JWT
|
- name: AUTOMATION_USER_JWT
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
@ -53,6 +55,6 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 150m
|
cpu: 150m
|
||||||
memory: 256Mi
|
memory: 512Mi
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
|
|
||||||
|
|
|
@ -47,16 +47,16 @@ spec:
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
key: jwtSecret
|
key: automationUserJwt
|
||||||
- name: POSTGREST_URL
|
- name: POSTGREST_URL
|
||||||
value: http://postgrest.futureporn.svc.cluster.local:9000
|
value: "{{ .Values.postgrest.url }}"
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: "{{ .Values.capture.api.port }}"
|
value: "{{ .Values.capture.api.port }}"
|
||||||
- name: S3_ENDPOINT
|
- name: S3_ENDPOINT
|
||||||
value: "{{ .Values.s3.endpoint }}"
|
value: "{{ .Values.s3.endpoint }}"
|
||||||
- name: S3_REGION
|
- name: S3_REGION
|
||||||
value: "{{ .Values.s3.region }}"
|
value: "{{ .Values.s3.region }}"
|
||||||
- name: S3_BUCKET_NAME
|
- name: S3_BUCKET
|
||||||
value: "{{ .Values.s3.buckets.usc }}"
|
value: "{{ .Values.s3.buckets.usc }}"
|
||||||
- name: S3_ACCESS_KEY_ID
|
- name: S3_ACCESS_KEY_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
|
@ -112,5 +112,5 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 128Mi
|
memory: 256Mi
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
|
@ -67,6 +67,7 @@ bot:
|
||||||
imageName: fp/bot
|
imageName: fp/bot
|
||||||
replicas: 1
|
replicas: 1
|
||||||
postgrest:
|
postgrest:
|
||||||
|
url: http://postgrest.futureporn.svc.cluster.local:9000
|
||||||
image: postgrest/postgrest
|
image: postgrest/postgrest
|
||||||
replicas: 1
|
replicas: 1
|
||||||
port: 9000
|
port: 9000
|
||||||
|
|
|
@ -8,6 +8,7 @@ ENTRYPOINT ["pnpm"]
|
||||||
FROM base AS install
|
FROM base AS install
|
||||||
COPY pnpm-lock.yaml .npmrc package.json .
|
COPY pnpm-lock.yaml .npmrc package.json .
|
||||||
COPY ./services/bot/ ./services/bot/
|
COPY ./services/bot/ ./services/bot/
|
||||||
|
COPY ./packages/types/ ./packages/types/
|
||||||
|
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --recursive --frozen-lockfile --prefer-offline
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --recursive --frozen-lockfile --prefer-offline
|
||||||
|
|
|
@ -3,23 +3,27 @@ import 'dotenv/config'
|
||||||
|
|
||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
|
|
||||||
export class ExhaustedRetries extends Error {
|
export class ExhaustedRetriesError extends Error {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super(message)
|
super(message)
|
||||||
Object.setPrototypeOf(this, ExhaustedRetries.prototype)
|
Object.setPrototypeOf(this, ExhaustedRetriesError.prototype)
|
||||||
|
this.name = this.constructor.name
|
||||||
|
this.message = `ExhaustedRetries: We retried the request the maximum amount of times. maxRetries of ${maxRetries} was reached.`
|
||||||
}
|
}
|
||||||
getErrorMessage() {
|
getErrorMessage() {
|
||||||
return `ExhaustedRetries: We retried the request the maximum amount of times. maxRetries of ${maxRetries} was reached.`
|
return this.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RoomOffline extends Error {
|
export class RoomOfflineError extends Error {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super(message)
|
super(message)
|
||||||
Object.setPrototypeOf(this, ExhaustedRetries.prototype)
|
Object.setPrototypeOf(this, RoomOfflineError.prototype)
|
||||||
|
this.name = this.constructor.name
|
||||||
|
this.message = `RoomOffline. ${this.message}`
|
||||||
}
|
}
|
||||||
getErrorMessage() {
|
getErrorMessage() {
|
||||||
return `RoomOffline. ${this.message}`
|
return this.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,16 +41,16 @@ export async function getPlaylistUrl (roomUrl: string, proxy = false, retries =
|
||||||
// we were likely blocked by Cloudflare
|
// we were likely blocked by Cloudflare
|
||||||
// we make the request a second time, this time via proxy
|
// we make the request a second time, this time via proxy
|
||||||
if (retries < maxRetries) return getPlaylistUrl(roomUrl, true, retries+=1);
|
if (retries < maxRetries) return getPlaylistUrl(roomUrl, true, retries+=1);
|
||||||
else throw new ExhaustedRetries();
|
else throw new ExhaustedRetriesError();
|
||||||
} else if (output.match(/Unable to find stream URL/)) {
|
} else if (output.match(/Unable to find stream URL/)) {
|
||||||
// sometimes this happens. a retry is in order.
|
// sometimes this happens. a retry is in order.
|
||||||
if (retries < maxRetries) return getPlaylistUrl(roomUrl, proxy, retries+=1);
|
if (retries < maxRetries) return getPlaylistUrl(roomUrl, proxy, retries+=1);
|
||||||
else throw new ExhaustedRetries()
|
else throw new ExhaustedRetriesError()
|
||||||
} else if (code === 0 && output.match(/https:\/\/.*\.m3u8/)) {
|
} else if (code === 0 && output.match(/https:\/\/.*\.m3u8/)) {
|
||||||
// this must be an OK result with a playlist
|
// this must be an OK result with a playlist
|
||||||
return output
|
return output
|
||||||
} else if (code === 1 && output.match(/Room is currently offline/)) {
|
} else if (code === 1 && output.match(/Room is currently offline/)) {
|
||||||
throw new RoomOffline()
|
throw new RoomOfflineError()
|
||||||
} else {
|
} else {
|
||||||
console.error('exotic scenario')
|
console.error('exotic scenario')
|
||||||
const msg = `We encountered an exotic scenario where code=${code} and output=${output}. Admin: please patch the code to handle this scenario.`
|
const msg = `We encountered an exotic scenario where code=${code} and output=${output}. Admin: please patch the code to handle this scenario.`
|
||||||
|
|
|
@ -4,6 +4,8 @@ export as namespace Futureporn;
|
||||||
|
|
||||||
declare namespace Futureporn {
|
declare namespace Futureporn {
|
||||||
|
|
||||||
|
type RecordingState = 'pending' | 'recording' | 'aborted' | 'ended'
|
||||||
|
|
||||||
|
|
||||||
interface IMuxAsset {
|
interface IMuxAsset {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Warn: no test specified\" && exit 0",
|
"test": "echo \"Warn: no test specified\" && exit 0",
|
||||||
"build": "tsc --build",
|
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"superclean": "rm -rf node_modules && rm -rf pnpm-lock.yaml && rm -rf dist"
|
"superclean": "rm -rf node_modules && rm -rf pnpm-lock.yaml && rm -rf dist"
|
||||||
},
|
},
|
||||||
|
|
|
@ -50,7 +50,8 @@ kubectl --namespace futureporn create secret generic pgadmin4 \
|
||||||
kubectl --namespace futureporn delete secret postgrest --ignore-not-found
|
kubectl --namespace futureporn delete secret postgrest --ignore-not-found
|
||||||
kubectl --namespace futureporn create secret generic postgrest \
|
kubectl --namespace futureporn create secret generic postgrest \
|
||||||
--from-literal=dbUri=${PGRST_DB_URI} \
|
--from-literal=dbUri=${PGRST_DB_URI} \
|
||||||
--from-literal=jwtSecret=${PGRST_JWT_SECRET}
|
--from-literal=jwtSecret=${PGRST_JWT_SECRET} \
|
||||||
|
--from-literal=automationUserJwt=${AUTOMATION_USER_JWT}
|
||||||
|
|
||||||
kubectl --namespace futureporn delete secret capture --ignore-not-found
|
kubectl --namespace futureporn delete secret capture --ignore-not-found
|
||||||
kubectl --namespace futureporn create secret generic capture \
|
kubectl --namespace futureporn create secret generic capture \
|
||||||
|
|
|
@ -70,9 +70,9 @@ kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PAS
|
||||||
IS_TEMPLATE = False;"
|
IS_TEMPLATE = False;"
|
||||||
|
|
||||||
|
|
||||||
## Create PgBoss db (for backend tasks)
|
## Create graphile_worker db (for backend tasks)
|
||||||
kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "\
|
kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "\
|
||||||
CREATE DATABASE pgboss \
|
CREATE DATABASE graphile_worker \
|
||||||
WITH \
|
WITH \
|
||||||
OWNER = postgres \
|
OWNER = postgres \
|
||||||
ENCODING = 'UTF8' \
|
ENCODING = 'UTF8' \
|
||||||
|
@ -97,10 +97,11 @@ kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PAS
|
||||||
|
|
||||||
|
|
||||||
## grant futureporn user all privs
|
## grant futureporn user all privs
|
||||||
kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "\
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE pgboss TO futureporn;"
|
|
||||||
kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "\
|
kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "\
|
||||||
GRANT ALL PRIVILEGES ON DATABASE postgrest TO futureporn;"
|
GRANT ALL PRIVILEGES ON DATABASE postgrest TO futureporn;"
|
||||||
|
kubectl -n futureporn exec ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "\
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE graphile_worker TO futureporn;"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## import schema
|
## import schema
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Warn: no test specified\" && exit 0",
|
"test": "echo \"Warn: no test specified\" && exit 0",
|
||||||
"start": "node ./dist/index.js",
|
"start": "node ./dist/index.js",
|
||||||
"dev.nodemon": "nodemon --ext js,ts,json,yaml --exec \"node --loader ts-node/esm --disable-warning=ExperimentalWarning ./src/index.ts\"",
|
"dev.nodemon": "nodemon --legacy-watch --ext js,ts --watch ./src --exec \"node --loader ts-node/esm --disable-warning=ExperimentalWarning ./src/index.ts\"",
|
||||||
"dev": "tsx --watch ./src/index.ts",
|
"dev": "tsx --watch ./src/index.ts",
|
||||||
"build": "tsc --build",
|
"build": "tsc --build",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
|
@ -24,6 +24,7 @@
|
||||||
"graphile-worker": "^0.16.6"
|
"graphile-worker": "^0.16.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@futureporn/types": "workspace:^",
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.16.2",
|
||||||
|
|
|
@ -21,12 +21,15 @@ importers:
|
||||||
specifier: ^0.16.6
|
specifier: ^0.16.6
|
||||||
version: 0.16.6(typescript@5.5.4)
|
version: 0.16.6(typescript@5.5.4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@futureporn/types':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/types
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.4
|
specifier: ^3.1.4
|
||||||
version: 3.1.4
|
version: 3.1.4
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@22.0.0)(typescript@5.5.4)
|
version: 10.9.2(@types/node@22.1.0)(typescript@5.5.4)
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.16.2
|
specifier: ^4.16.2
|
||||||
version: 4.16.2
|
version: 4.16.2
|
||||||
|
@ -306,17 +309,20 @@ packages:
|
||||||
'@types/node@22.0.0':
|
'@types/node@22.0.0':
|
||||||
resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==}
|
resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==}
|
||||||
|
|
||||||
|
'@types/node@22.1.0':
|
||||||
|
resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==}
|
||||||
|
|
||||||
'@types/pg@8.11.6':
|
'@types/pg@8.11.6':
|
||||||
resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==}
|
resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==}
|
||||||
|
|
||||||
'@types/semver@7.5.8':
|
'@types/semver@7.5.8':
|
||||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||||
|
|
||||||
'@types/ws@8.5.11':
|
'@types/ws@8.5.12':
|
||||||
resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==}
|
resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
|
||||||
|
|
||||||
'@vladfrangu/async_event_emitter@2.4.4':
|
'@vladfrangu/async_event_emitter@2.4.5':
|
||||||
resolution: {integrity: sha512-ZL62PFXEIeGUI8btfJ5S8Flc286eU1ZUSjwyFQtIGXfRUDPZKO+CDJMYb1R71LjGWRZ4n202O+a6FGjsgTw58g==}
|
resolution: {integrity: sha512-J7T3gUr3Wz0l7Ni1f9upgBZ7+J22/Q1B7dl0X6fG+fTsD+H+31DIosMHj4Um1dWQwqbcQ3oQf+YS2foYkDc9cQ==}
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
||||||
|
|
||||||
acorn-walk@8.3.3:
|
acorn-walk@8.3.3:
|
||||||
|
@ -767,6 +773,9 @@ packages:
|
||||||
undici-types@6.11.1:
|
undici-types@6.11.1:
|
||||||
resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==}
|
resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==}
|
||||||
|
|
||||||
|
undici-types@6.13.0:
|
||||||
|
resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==}
|
||||||
|
|
||||||
undici@6.13.0:
|
undici@6.13.0:
|
||||||
resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==}
|
resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==}
|
||||||
engines: {node: '>=18.0'}
|
engines: {node: '>=18.0'}
|
||||||
|
@ -854,7 +863,7 @@ snapshots:
|
||||||
'@discordjs/util': 1.1.0
|
'@discordjs/util': 1.1.0
|
||||||
'@sapphire/async-queue': 1.5.3
|
'@sapphire/async-queue': 1.5.3
|
||||||
'@sapphire/snowflake': 3.5.3
|
'@sapphire/snowflake': 3.5.3
|
||||||
'@vladfrangu/async_event_emitter': 2.4.4
|
'@vladfrangu/async_event_emitter': 2.4.5
|
||||||
discord-api-types: 0.37.83
|
discord-api-types: 0.37.83
|
||||||
magic-bytes.js: 1.10.0
|
magic-bytes.js: 1.10.0
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
|
@ -868,8 +877,8 @@ snapshots:
|
||||||
'@discordjs/rest': 2.3.0
|
'@discordjs/rest': 2.3.0
|
||||||
'@discordjs/util': 1.1.0
|
'@discordjs/util': 1.1.0
|
||||||
'@sapphire/async-queue': 1.5.3
|
'@sapphire/async-queue': 1.5.3
|
||||||
'@types/ws': 8.5.11
|
'@types/ws': 8.5.12
|
||||||
'@vladfrangu/async_event_emitter': 2.4.4
|
'@vladfrangu/async_event_emitter': 2.4.5
|
||||||
discord-api-types: 0.37.83
|
discord-api-types: 0.37.83
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
ws: 8.18.0
|
ws: 8.18.0
|
||||||
|
@ -992,6 +1001,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.11.1
|
undici-types: 6.11.1
|
||||||
|
|
||||||
|
'@types/node@22.1.0':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.13.0
|
||||||
|
|
||||||
'@types/pg@8.11.6':
|
'@types/pg@8.11.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.0.0
|
'@types/node': 22.0.0
|
||||||
|
@ -1000,11 +1013,11 @@ snapshots:
|
||||||
|
|
||||||
'@types/semver@7.5.8': {}
|
'@types/semver@7.5.8': {}
|
||||||
|
|
||||||
'@types/ws@8.5.11':
|
'@types/ws@8.5.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.0.0
|
'@types/node': 22.1.0
|
||||||
|
|
||||||
'@vladfrangu/async_event_emitter@2.4.4': {}
|
'@vladfrangu/async_event_emitter@2.4.5': {}
|
||||||
|
|
||||||
acorn-walk@8.3.3:
|
acorn-walk@8.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1419,14 +1432,14 @@ snapshots:
|
||||||
|
|
||||||
ts-mixer@6.0.4: {}
|
ts-mixer@6.0.4: {}
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@22.0.0)(typescript@5.5.4):
|
ts-node@10.9.2(@types/node@22.1.0)(typescript@5.5.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
'@tsconfig/node10': 1.0.11
|
'@tsconfig/node10': 1.0.11
|
||||||
'@tsconfig/node12': 1.0.11
|
'@tsconfig/node12': 1.0.11
|
||||||
'@tsconfig/node14': 1.0.3
|
'@tsconfig/node14': 1.0.3
|
||||||
'@tsconfig/node16': 1.0.4
|
'@tsconfig/node16': 1.0.4
|
||||||
'@types/node': 22.0.0
|
'@types/node': 22.1.0
|
||||||
acorn: 8.12.1
|
acorn: 8.12.1
|
||||||
acorn-walk: 8.3.3
|
acorn-walk: 8.3.3
|
||||||
arg: 4.1.3
|
arg: 4.1.3
|
||||||
|
@ -1454,6 +1467,8 @@ snapshots:
|
||||||
|
|
||||||
undici-types@6.11.1: {}
|
undici-types@6.11.1: {}
|
||||||
|
|
||||||
|
undici-types@6.13.0: {}
|
||||||
|
|
||||||
undici@6.13.0: {}
|
undici@6.13.0: {}
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1: {}
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
|
@ -42,9 +42,9 @@ export default {
|
||||||
// cols can be 5 high
|
// cols can be 5 high
|
||||||
// rows can be 5 wide
|
// rows can be 5 wide
|
||||||
const statusEmbed = new EmbedBuilder()
|
const statusEmbed = new EmbedBuilder()
|
||||||
.setTitle('Pending')
|
.setTitle('Pending')
|
||||||
.setDescription('Waiting for a worker to accept the job.')
|
.setDescription('Waiting for a worker to accept the job.')
|
||||||
.setColor(2326507)
|
.setColor(2326507)
|
||||||
|
|
||||||
const buttonRow = new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
const buttonRow = new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
||||||
.addComponents([
|
.addComponents([
|
||||||
|
@ -73,7 +73,8 @@ export default {
|
||||||
|
|
||||||
const message = await idk.fetch()
|
const message = await idk.fetch()
|
||||||
const discordMessageId = message.id
|
const discordMessageId = message.id
|
||||||
await workerUtils.addJob('startRecording', { url, discordMessageId })
|
await workerUtils.addJob('startRecording', { url, discordMessageId }, { maxAttempts: 3 })
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Client, Events, type Interaction } from 'discord.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: Events.ClientReady,
|
||||||
|
once: true,
|
||||||
|
execute(client: Client) {
|
||||||
|
console.log(`Ready! Logged in as ${client?.user?.tag}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Events, type Interaction, Client, Collection } from 'discord.js';
|
||||||
|
import type { WorkerUtils } from 'graphile-worker';
|
||||||
|
|
||||||
|
interface ExtendedClient extends Client {
|
||||||
|
commands: Collection<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: Events.InteractionCreate,
|
||||||
|
once: false,
|
||||||
|
async execute(interaction: Interaction, workerUtils: WorkerUtils) {
|
||||||
|
// if (!interaction.isChatInputCommand()) return;
|
||||||
|
// console.log(interaction.client)
|
||||||
|
// const command = interaction.client.commands.get(interaction.commandName);
|
||||||
|
if (interaction.isButton()) {
|
||||||
|
console.log(`the interaction is a button type with customId=${interaction.customId}, message.id=${interaction.message.id}, user=${interaction.user.id} (${interaction.user.globalName})`)
|
||||||
|
if (interaction.customId === 'stop') {
|
||||||
|
interaction.reply('[stop] IDK IDK IDK ??? @todo')
|
||||||
|
workerUtils.addJob('stopRecording', { discordMessageId: interaction.message.id, userId: interaction.user.id })
|
||||||
|
} else if (interaction.customId === 'retry') {
|
||||||
|
interaction.reply('[retry] IDK IDK IDK ??? @todo')
|
||||||
|
workerUtils.addJob('startRecording', { discordMessageId: interaction.message.id, userId: interaction.user.id })
|
||||||
|
} else {
|
||||||
|
console.error(`this button's customId=${interaction.customId} did not match one of the known customIds`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (interaction.isChatInputCommand()) {
|
||||||
|
console.log(`the interaction is a ChatInputCommandInteraction with commandName=${interaction.commandName}, user=${interaction.user.id} (${interaction.user.globalName})`)
|
||||||
|
const client = interaction.client as ExtendedClient
|
||||||
|
const command = client.commands.get(interaction.commandName);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
command.execute({ interaction, workerUtils })
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// const { Events } = require('discord.js');
|
||||||
|
|
||||||
|
// module.exports = {
|
||||||
|
// name: Events.ClientReady,
|
||||||
|
// once: true,
|
||||||
|
// execute(client) {
|
||||||
|
// console.log(`Ready! Logged in as ${client.user.tag}`);
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// client.on(Events.InteractionCreate, interaction => {
|
||||||
|
// if (interaction.isChatInputCommand()) {
|
||||||
|
// const { commandName } = interaction;
|
||||||
|
// console.log(`Received interaction with commandName=${commandName}`)
|
||||||
|
// const cmd = commands.find((c) => c.data.name === commandName)
|
||||||
|
// if (!cmd) {
|
||||||
|
// console.log(`no command handler matches commandName=${commandName}`)
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// cmd.execute({ interaction, workerUtils })
|
||||||
|
// } else {
|
||||||
|
// // probably a ButtonInteraction
|
||||||
|
// console.log(interaction)
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Client, Events, MessageReaction, User, type Interaction } from 'discord.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: Events.MessageReactionAdd,
|
||||||
|
once: false,
|
||||||
|
async execute(reaction: MessageReaction, user: 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!`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
import { type ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Partials } from 'discord.js'
|
import { type ChatInputCommandInteraction, Client, GatewayIntentBits, Partials, Collection } from 'discord.js'
|
||||||
import loadCommands from './loadCommands.js'
|
import loadCommands from './loadCommands.js'
|
||||||
import deployCommands from './deployCommands.js'
|
import deployCommands from './deployCommands.js'
|
||||||
import discordMessageUpdate from './tasks/discordMessageUpdate.js'
|
import discordMessageUpdate from './tasks/discordMessageUpdate.js'
|
||||||
import { makeWorkerUtils, type WorkerUtils } from 'graphile-worker'
|
import { makeWorkerUtils, type WorkerUtils, type RunnerOptions, run } from 'graphile-worker'
|
||||||
|
import loadEvents from './loadEvents.js'
|
||||||
|
|
||||||
export interface ExecuteArguments {
|
export interface ExecuteArguments {
|
||||||
interaction: ChatInputCommandInteraction;
|
interaction: ChatInputCommandInteraction;
|
||||||
|
@ -15,9 +16,31 @@ if (!process.env.DISCORD_TOKEN) throw new Error("DISCORD_TOKEN was missing from
|
||||||
if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env");
|
if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env");
|
||||||
if (!process.env.WORKER_CONNECTION_STRING) throw new Error("WORKER_CONNECTION_STRING was missing from env");
|
if (!process.env.WORKER_CONNECTION_STRING) throw new Error("WORKER_CONNECTION_STRING was missing from env");
|
||||||
|
|
||||||
|
const preset: GraphileConfig.Preset = {
|
||||||
|
worker: {
|
||||||
|
connectionString: process.env.WORKER_CONNECTION_STRING,
|
||||||
|
concurrentJobs: 1,
|
||||||
|
fileExtensions: [".js", ".ts"]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
async function setupGraphileWorker() {
|
async function setupGraphileWorker() {
|
||||||
|
const runnerOptions: RunnerOptions = {
|
||||||
|
preset,
|
||||||
|
taskList: {
|
||||||
|
'discordMessageUpdate': discordMessageUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = await run(runnerOptions)
|
||||||
|
if (!runner) throw new Error('failed to initialize graphile worker');
|
||||||
|
await runner.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function setupWorkerUtils() {
|
||||||
const workerUtils = await makeWorkerUtils({
|
const workerUtils = await makeWorkerUtils({
|
||||||
connectionString: process.env.WORKER_CONNECTION_STRING!,
|
preset
|
||||||
});
|
});
|
||||||
await workerUtils.migrate()
|
await workerUtils.migrate()
|
||||||
return workerUtils
|
return workerUtils
|
||||||
|
@ -30,7 +53,7 @@ async function setupDiscordBot(commands: any[], workerUtils: WorkerUtils) {
|
||||||
|
|
||||||
|
|
||||||
console.log(`Create a new client instance`)
|
console.log(`Create a new client instance`)
|
||||||
const client = new Client({
|
let client: any = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
|
@ -42,67 +65,14 @@ async function setupDiscordBot(commands: any[], workerUtils: WorkerUtils) {
|
||||||
Partials.Reaction,
|
Partials.Reaction,
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
client.commands = new Collection();
|
||||||
|
commands.forEach((c) => client.commands.set(c.data.name, c))
|
||||||
client.on(Events.InteractionCreate, interaction => {
|
|
||||||
if (!interaction.isChatInputCommand()) return;
|
|
||||||
const { commandName } = interaction;
|
|
||||||
console.log(`Received interaction with commandName=${commandName}`)
|
|
||||||
const cmd = commands.find((c) => c.data.name === commandName)
|
|
||||||
if (!cmd) {
|
|
||||||
console.log(`no command handler matches commandName=${commandName}`)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cmd.execute({ interaction, workerUtils })
|
|
||||||
});
|
|
||||||
// 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}`);
|
|
||||||
// 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
|
// Log in to Discord with your client's token
|
||||||
client.login(process.env.DISCORD_TOKEN);
|
client.login(process.env.DISCORD_TOKEN);
|
||||||
|
await loadEvents(client, workerUtils)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +82,9 @@ async function main() {
|
||||||
if (!commands) throw new Error('there were no commands available to be loaded.');
|
if (!commands) throw new Error('there were no commands available to be loaded.');
|
||||||
await deployCommands(commands.map((c) => c.data.toJSON()))
|
await deployCommands(commands.map((c) => c.data.toJSON()))
|
||||||
console.log(`${commands.length} commands deployed: ${commands.map((c) => c.data.name).join(', ')}`)
|
console.log(`${commands.length} commands deployed: ${commands.map((c) => c.data.name).join(', ')}`)
|
||||||
const workerUtils = await setupGraphileWorker()
|
const workerUtils = await setupWorkerUtils()
|
||||||
setupDiscordBot(commands, workerUtils)
|
setupDiscordBot(commands, workerUtils)
|
||||||
|
setupGraphileWorker()
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
|
|
|
@ -14,13 +14,13 @@ export default async function loadCommands(): Promise<any[]> {
|
||||||
for (const folder of commandFolders) {
|
for (const folder of commandFolders) {
|
||||||
const commandsPath = path.join(foldersPath, folder);
|
const commandsPath = path.join(foldersPath, folder);
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
||||||
console.log(`commandFiles=${commandFiles}`)
|
console.log(`commandFiles=${commandFiles}`);
|
||||||
// console.log(`Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment`)
|
// console.log(`Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment`)
|
||||||
for (const file of commandFiles) {
|
for (const file of commandFiles) {
|
||||||
const filePath = path.join(commandsPath, file);
|
const filePath = path.join(commandsPath, file);
|
||||||
const command = (await import(filePath)).default;
|
const command = (await import(filePath)).default;
|
||||||
// console.log(command)
|
// console.log(command)
|
||||||
if ('data' in command && 'execute' in command) {
|
if (command?.data && command?.execute) {
|
||||||
commands.push(command);
|
commands.push(command);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import type { Client } from 'discord.js';
|
||||||
|
import type { WorkerUtils } from 'graphile-worker';
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default async function loadEvents(client: Client, workerUtils: WorkerUtils) {
|
||||||
|
console.log(`loading events`);
|
||||||
|
const eventsPath = path.join(__dirname, 'events');
|
||||||
|
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
||||||
|
console.log(`eventFiles=${eventFiles}`);
|
||||||
|
for (const file of eventFiles) {
|
||||||
|
const filePath = path.join(eventsPath, file);
|
||||||
|
const event = (await import(filePath)).default;
|
||||||
|
if (event.once) {
|
||||||
|
client.once(event.name, (...args) => event.execute(...args, workerUtils));
|
||||||
|
} else {
|
||||||
|
client.on(event.name, (...args) => event.execute(...args, workerUtils));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,106 +0,0 @@
|
||||||
import 'dotenv/config'
|
|
||||||
import { type Task, type WorkerUtils } from 'graphile-worker';
|
|
||||||
import { type ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Partials, Message, TextChannel } from 'discord.js'
|
|
||||||
|
|
||||||
|
|
||||||
// export interface DiscordMessageUpdateJob extends Job {
|
|
||||||
// data: {
|
|
||||||
// captureJobId: string;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
interface Payload {
|
|
||||||
discord_message_id: string;
|
|
||||||
capture_job_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertPayload(payload: any): asserts payload is Payload {
|
|
||||||
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
|
|
||||||
if (typeof payload.discord_message_id !== "string") throw new Error("invalid discord_message_id");
|
|
||||||
if (typeof payload.capture_job_id.to !== "string") throw new Error("invalid capture_job_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`);
|
|
||||||
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");
|
|
||||||
if (!process.env.DISCORD_GUILD_ID) throw new Error("DISCORD_GUILD_ID was missing from env");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* discordMessageUpdate is the task where we edit a previously sent discord message to display new status information sent to us from @futureporn/capture
|
|
||||||
*
|
|
||||||
* Sometimes the update is changing the state, one of Pending|Recording|Aborted|Ended.
|
|
||||||
* Sometimes the update is updating the Filesize of the recording in-progress
|
|
||||||
* Sometimes the update is adding a thumbnail image to the message
|
|
||||||
*/
|
|
||||||
export default async function discordMessageUpdate <Task> (payload: Payload) {
|
|
||||||
assertPayload(payload)
|
|
||||||
// const { captureJobId } = job.data
|
|
||||||
console.log(`discordMessageUpdate job has begun with captureJobId=${payload.capture_job_id}`)
|
|
||||||
|
|
||||||
|
|
||||||
// // find the discord_interactions record containing the captureJobId
|
|
||||||
// const res = await fetch(`http://postgrest.futureporn.svc.cluster.local:9000/discord_interactions?capture_job_id=eq.${captureJobId}`)
|
|
||||||
// if (!res.ok) throw new Error('failed to fetch the discord_interactions');
|
|
||||||
// const body = await res.json() as DiscordInteraction
|
|
||||||
// console.log('discord_interactions as follows')
|
|
||||||
// console.log(body)
|
|
||||||
|
|
||||||
// // create a discord.js client
|
|
||||||
// const client = new Client({
|
|
||||||
// intents: [
|
|
||||||
// GatewayIntentBits.Guilds,
|
|
||||||
// GatewayIntentBits.GuildMessages
|
|
||||||
// ],
|
|
||||||
// partials: [
|
|
||||||
// Partials.Message,
|
|
||||||
// Partials.Channel,
|
|
||||||
// ]
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // const messageManager = client.
|
|
||||||
// // const guild = client.guilds.cache.get(process.env.DISCORD_GUILD_ID!);
|
|
||||||
// // if (!guild) throw new Error('guild was undefined')
|
|
||||||
|
|
||||||
|
|
||||||
// const channel = await client.channels.fetch(process.env.DISCORD_CHANNEL_ID!) as TextChannel
|
|
||||||
// if (!channel) throw new Error(`discord channel was undefined`);
|
|
||||||
|
|
||||||
// // console.log('we got the following channel')
|
|
||||||
// // console.log(channel)
|
|
||||||
|
|
||||||
// const message = await channel.messages.fetch(body.discord_message_id)
|
|
||||||
|
|
||||||
// console.log('we got the following message')
|
|
||||||
// console.log(message)
|
|
||||||
|
|
||||||
// get the message
|
|
||||||
// client.channel.messages.get()
|
|
||||||
// client.rest.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const message: Message = {
|
|
||||||
// id: body.discord_message_id
|
|
||||||
// };
|
|
||||||
// await message.fetchReference()
|
|
||||||
// message.edit('test message update payload thingy')
|
|
||||||
|
|
||||||
|
|
||||||
// client.rest.updateMessage(message, "My new content");
|
|
||||||
// TextChannel.fetchMessage(msgId).then(console.log);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TextChannel
|
|
||||||
|
|
||||||
// channel.messages.fetch(`Your Message ID`).then(message => {
|
|
||||||
// message.edit("New message Text");
|
|
||||||
// }).catch(err => {
|
|
||||||
// console.error(err);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// using the discord_interaction's discord_message_id, use discord.js to update the discord message.
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
import 'dotenv/config'
|
||||||
|
import type { RecordingState } from '@futureporn/types'
|
||||||
|
import { type Task, type Helpers } from 'graphile-worker';
|
||||||
|
import {
|
||||||
|
Client,
|
||||||
|
GatewayIntentBits,
|
||||||
|
TextChannel,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
type MessageActionRowComponentBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
EmbedBuilder,
|
||||||
|
Guild
|
||||||
|
} from 'discord.js';
|
||||||
|
|
||||||
|
// export interface DiscordMessageUpdateJob extends Job {
|
||||||
|
// data: {
|
||||||
|
// captureJobId: string;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
interface Payload {
|
||||||
|
recordId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
|
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
|
||||||
|
if (!payload.recordId) throw new Error(`recordId was absent in the payload`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`);
|
||||||
|
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");
|
||||||
|
if (!process.env.DISCORD_GUILD_ID) throw new Error("DISCORD_GUILD_ID was missing from env");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function editDiscordMessage({ helpers, state, discordMessageId }: { helpers: Helpers, state: RecordingState, discordMessageId: string }) {
|
||||||
|
|
||||||
|
// const { captureJobId } = job.data
|
||||||
|
helpers.logger.info(`discordMessageUpdate job has begun with discordMessageId=${discordMessageId}, state=${state}`)
|
||||||
|
|
||||||
|
|
||||||
|
// create a discord.js client
|
||||||
|
const client = new Client({
|
||||||
|
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log in to Discord with your client's token
|
||||||
|
client.login(process.env.DISCORD_TOKEN);
|
||||||
|
|
||||||
|
|
||||||
|
const guild = await client.guilds.fetch(process.env.DISCORD_GUILD_ID!) as Guild
|
||||||
|
if (!guild) throw new Error('guild was undefined');
|
||||||
|
|
||||||
|
helpers.logger.info('here is the guild as follows')
|
||||||
|
helpers.logger.info(guild.toString())
|
||||||
|
helpers.logger.info(`fetching discord channel id=${process.env.DISCORD_CHANNEL_ID} from discord guild`)
|
||||||
|
const channel = await client.channels.fetch(process.env.DISCORD_CHANNEL_ID!) as TextChannel
|
||||||
|
if (!channel) throw new Error(`discord channel was undefined`);
|
||||||
|
|
||||||
|
const message = await channel.messages.fetch(discordMessageId)
|
||||||
|
|
||||||
|
helpers.logger.info(`the following is the message taht we have fetched`)
|
||||||
|
helpers.logger.info(message.toString())
|
||||||
|
|
||||||
|
const statusEmbed = getStatusEmbed(state)
|
||||||
|
const buttonRow = getButtonRow(state)
|
||||||
|
|
||||||
|
|
||||||
|
// const embed = new EmbedBuilder().setTitle('Attachments');
|
||||||
|
|
||||||
|
|
||||||
|
const updatedMessage = {
|
||||||
|
embeds: [
|
||||||
|
statusEmbed
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
buttonRow
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
message.edit(updatedMessage)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getRecordFromDatabase(recordId: number) {
|
||||||
|
const res = await fetch(`${process.env.POSTGREST_URL}/records?id=eq.${recordId}`)
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`failed fetching record ${recordId}. status=${res.status}, statusText=${res.statusText}`)
|
||||||
|
}
|
||||||
|
const body = await res.json() as any
|
||||||
|
return body[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updateDiscordMessage is the task where we edit a previously sent discord message to display
|
||||||
|
* the most up-to-date status information from the database
|
||||||
|
*
|
||||||
|
* Sometimes the update is changing the state, one of Pending|Recording|Aborted|Ended.
|
||||||
|
* Sometimes the update is updating the Filesize of the recording in-progress
|
||||||
|
* Sometimes the update is adding a thumbnail image to the message
|
||||||
|
*/
|
||||||
|
export const updateDiscordMessage: Task = async function (payload, helpers: Helpers) {
|
||||||
|
try {
|
||||||
|
assertPayload(payload)
|
||||||
|
const record = await getRecordFromDatabase(payload.recordId)
|
||||||
|
const { discordMessageId, state } = record
|
||||||
|
editDiscordMessage({ helpers, state, discordMessageId })
|
||||||
|
} catch (e) {
|
||||||
|
helpers.logger.error(`caught an error during updateDiscordMessage. e=${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusEmbed(state: RecordingState) {
|
||||||
|
let title, description, color;
|
||||||
|
if (state === 'pending') {
|
||||||
|
title = "Pending"
|
||||||
|
description = "Waiting for a worker to accept the job."
|
||||||
|
color = 2326507
|
||||||
|
} else if (state === 'recording') {
|
||||||
|
title = 'Recording'
|
||||||
|
description = 'The stream is being recorded.'
|
||||||
|
color = 392960
|
||||||
|
} else if (state === 'aborted') {
|
||||||
|
title = "Aborted"
|
||||||
|
description = "The recording was stopped by the user."
|
||||||
|
color = 8289651
|
||||||
|
} else if (state === 'ended') {
|
||||||
|
title = "Ended"
|
||||||
|
description = "The recording has stopped."
|
||||||
|
color = 10855845
|
||||||
|
} else {
|
||||||
|
title = 'Unknown'
|
||||||
|
description = 'The recording is in an unknown state? (this is a bug.)'
|
||||||
|
color = 10855845
|
||||||
|
}
|
||||||
|
return new EmbedBuilder().setTitle(title).setDescription(description).setColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getButtonRow(state: RecordingState) {
|
||||||
|
let id, label, emoji, style;
|
||||||
|
|
||||||
|
if (state === 'pending') {
|
||||||
|
id = 'stop';
|
||||||
|
label = 'Cancel';
|
||||||
|
emoji = '❌';
|
||||||
|
style = ButtonStyle.Danger;
|
||||||
|
} else if (state === 'recording') {
|
||||||
|
id = 'stop';
|
||||||
|
label = 'Stop Recording';
|
||||||
|
emoji = '🛑';
|
||||||
|
style = ButtonStyle.Danger;
|
||||||
|
} else if (state === 'aborted') {
|
||||||
|
id = 'retry';
|
||||||
|
label = 'Retry Recording';
|
||||||
|
emoji = '🔄';
|
||||||
|
style = ButtonStyle.Success;
|
||||||
|
} else if (state === 'ended') {
|
||||||
|
id = 'download';
|
||||||
|
label = 'Download Recording';
|
||||||
|
emoji = '📥';
|
||||||
|
style = ButtonStyle.Primary;
|
||||||
|
} else {
|
||||||
|
id = 'unknown';
|
||||||
|
label = 'Unknown State';
|
||||||
|
emoji = '🤔';
|
||||||
|
style = ButtonStyle.Secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ActionRowBuilder<MessageActionRowComponentBuilder>()
|
||||||
|
.addComponents([
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(id)
|
||||||
|
.setLabel(label)
|
||||||
|
.setEmoji(emoji)
|
||||||
|
.setStyle(style),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default updateDiscordMessage
|
|
@ -21,6 +21,7 @@
|
||||||
"@aws-sdk/lib-storage": "^3.588.0",
|
"@aws-sdk/lib-storage": "^3.588.0",
|
||||||
"@aws-sdk/types": "^3.609.0",
|
"@aws-sdk/types": "^3.609.0",
|
||||||
"@futureporn/scout": "workspace:^",
|
"@futureporn/scout": "workspace:^",
|
||||||
|
"@futureporn/types": "workspace:^",
|
||||||
"@futureporn/utils": "workspace:^",
|
"@futureporn/utils": "workspace:^",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@types/chai": "^4.3.16",
|
"@types/chai": "^4.3.16",
|
||||||
|
|
|
@ -20,6 +20,9 @@ importers:
|
||||||
'@futureporn/scout':
|
'@futureporn/scout':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/scout
|
version: link:../../packages/scout
|
||||||
|
'@futureporn/types':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/types
|
||||||
'@futureporn/utils':
|
'@futureporn/utils':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/utils
|
version: link:../../packages/utils
|
||||||
|
|
|
@ -13,6 +13,10 @@ interface RecordBodyType {
|
||||||
url: string;
|
url: string;
|
||||||
discordMessageId: string;
|
discordMessageId: string;
|
||||||
}
|
}
|
||||||
|
interface MessageBodyType {
|
||||||
|
state: 'pending' | 'recording' | 'aborted' | 'ended';
|
||||||
|
discordMessageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
const build = function (opts: Record<string, any>={}, connectionString: string) {
|
const build = function (opts: Record<string, any>={}, connectionString: string) {
|
||||||
const app: ExtendedFastifyInstance = fastify(opts)
|
const app: ExtendedFastifyInstance = fastify(opts)
|
||||||
|
@ -21,6 +25,17 @@ const build = function (opts: Record<string, any>={}, connectionString: string)
|
||||||
app.get('/', async function (request, reply) {
|
app.get('/', async function (request, reply) {
|
||||||
return { app: '@futureporn/capture', version }
|
return { app: '@futureporn/capture', version }
|
||||||
})
|
})
|
||||||
|
app.put('/api/message', async function (request: FastifyRequest<{ Body: MessageBodyType }>, reply) {
|
||||||
|
const { state, discordMessageId } = request.body
|
||||||
|
if (app?.graphile) {
|
||||||
|
const jobId = await app.graphile.addJob('discordMessageUpdate', {
|
||||||
|
discordMessageId,
|
||||||
|
state
|
||||||
|
}, { maxAttempts: 3 })
|
||||||
|
} else {
|
||||||
|
console.error('app.graphile was missing')
|
||||||
|
}
|
||||||
|
})
|
||||||
app.post('/api/record', async function (request: FastifyRequest<{ Body: RecordBodyType }>, reply) {
|
app.post('/api/record', async function (request: FastifyRequest<{ Body: RecordBodyType }>, reply) {
|
||||||
const { url, discordMessageId } = request.body
|
const { url, discordMessageId } = request.body
|
||||||
console.log(`POST /api/record with url=${url}`)
|
console.log(`POST /api/record with url=${url}`)
|
||||||
|
@ -29,7 +44,7 @@ const build = function (opts: Record<string, any>={}, connectionString: string)
|
||||||
const jobId = await app.graphile.addJob('startRecording', {
|
const jobId = await app.graphile.addJob('startRecording', {
|
||||||
url,
|
url,
|
||||||
discordMessageId
|
discordMessageId
|
||||||
})
|
}, { maxAttempts: 3 })
|
||||||
return { jobId }
|
return { jobId }
|
||||||
} else {
|
} else {
|
||||||
console.error(`app.graphile was missing! Is the graphile worker plugin registered to the fastify instance?`)
|
console.error(`app.graphile was missing! Is the graphile worker plugin registered to the fastify instance?`)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Helpers, type Task } from 'graphile-worker'
|
import { Helpers, type Task } from 'graphile-worker'
|
||||||
import Record from '../Record.ts'
|
import Record from '../Record.ts'
|
||||||
import { getPlaylistUrl } from '@futureporn/scout/ytdlp.ts'
|
import { getPlaylistUrl } from '@futureporn/scout/ytdlp.ts'
|
||||||
|
import type { RecordingState } from '@futureporn/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* url is the URL to be recorded. Ex: chaturbate.com/projektmelody
|
* url is the URL to be recorded. Ex: chaturbate.com/projektmelody
|
||||||
|
@ -22,7 +22,7 @@ interface RecordingRecord {
|
||||||
function assertPayload(payload: any): asserts payload is Payload {
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
|
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
|
||||||
if (typeof payload.url !== "string") throw new Error("invalid url");
|
if (typeof payload.url !== "string") throw new Error("invalid url");
|
||||||
if (typeof payload.recordId !== "number") throw new Error("invalid recordId");
|
if (typeof payload.recordId !== "number") throw new Error(`invalid recordId=${payload.recordId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertEnv() {
|
function assertEnv() {
|
||||||
|
@ -44,13 +44,12 @@ async function getRecording(url: string, recordId: number, abortSignal: AbortSig
|
||||||
const s3Client = Record.makeS3Client({ accessKeyId, secretAccessKey, region, endpoint })
|
const s3Client = Record.makeS3Client({ accessKeyId, secretAccessKey, region, endpoint })
|
||||||
const inputStream = Record.getFFmpegStream({ url: playlistUrl })
|
const inputStream = Record.getFFmpegStream({ url: playlistUrl })
|
||||||
|
|
||||||
const record = new Record({ inputStream, bucket, s3Client, jobId: ''+recordId, abortSignal })
|
const record = new Record({ inputStream, bucket, s3Client, jobId: ''+recordId }) // @todo add abortsignal
|
||||||
record.start()
|
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIfAborted(recordId: number): Promise<boolean> {
|
async function checkIfAborted(recordId: number): Promise<boolean> {
|
||||||
const res = await fetch(`${process.env.POSTGREST_URL}/records?id.eq=${recordId}`, {
|
const res = await fetch(`${process.env.POSTGREST_URL}/records?id=eq.${recordId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accepts': 'application/json'
|
'Accepts': 'application/json'
|
||||||
|
@ -64,8 +63,26 @@ async function checkIfAborted(recordId: number): Promise<boolean> {
|
||||||
return body[0].isAborted
|
return body[0].isAborted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateDatabaseRecord({recordId, state, filesize}: { recordId: number, state: RecordingState, filesize: number }): Promise<void> {
|
||||||
|
const res = await fetch(`${process.env.POSTGREST_URL}/records?id=eq.${recordId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accepts': 'application/json',
|
||||||
|
'Prefer': 'return=representation'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ state, filesize })
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`failed to checkIfAborted. status=${res.status}, statusText=${res.statusText}`);
|
||||||
|
}
|
||||||
|
const body = await res.json() as RecordingRecord[];
|
||||||
|
if (!body[0]) throw new Error(`failed to get a record that matched recordId=${recordId}`)
|
||||||
|
return body[0].isAborted
|
||||||
|
}
|
||||||
|
|
||||||
export const record: Task = async function (payload, helpers) {
|
export const record: Task = async function (payload, helpers) {
|
||||||
|
console.log(payload)
|
||||||
assertPayload(payload)
|
assertPayload(payload)
|
||||||
assertEnv()
|
assertEnv()
|
||||||
const { url, recordId } = payload
|
const { url, recordId } = payload
|
||||||
|
@ -73,27 +90,39 @@ export const record: Task = async function (payload, helpers) {
|
||||||
let interval
|
let interval
|
||||||
try {
|
try {
|
||||||
const record = await getRecording(url, recordId, abortController.signal)
|
const record = await getRecording(url, recordId, abortController.signal)
|
||||||
// every 30s, poll db to see if our job has been aborted by the user
|
// every 30s, we
|
||||||
|
// 1. poll db to see if our job has been aborted by the user
|
||||||
|
// 2. update the db record with the RecordingState and filesize
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
const isAborted = await checkIfAborted(recordId)
|
try {
|
||||||
if (isAborted) {
|
helpers.logger.info(`checkIfAborted()`)
|
||||||
abortController.abort()
|
const isAborted = await checkIfAborted(recordId)
|
||||||
|
if (isAborted) {
|
||||||
|
abortController.abort()
|
||||||
|
}
|
||||||
|
let state: RecordingState = 'recording'
|
||||||
|
} catch (e) {
|
||||||
|
helpers.logger.error(`error while checking if this job was aborted. For sake of the recording in progress we are ignoring the following error. ${e}`)
|
||||||
}
|
}
|
||||||
}, 30000)
|
}, 30000)
|
||||||
|
|
||||||
|
// start recording and await the S3 upload being finished
|
||||||
|
await record.start()
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const recordId = await createRecordingRecord(payload, helpers)
|
// const recordId = await createRecordingRecord(payload, helpers)
|
||||||
// const { url } = payload;
|
// const { url } = payload;
|
||||||
// console.log(`@todo simulated start_recording with url=${url}, recordId=${recordId}`)
|
// console.log(`@todo simulated start_recording with url=${url}, recordId=${recordId}`)
|
||||||
// await helpers.addJob('record', { url, recordId })
|
// await helpers.addJob('record', { url, recordId })
|
||||||
}
|
}
|
||||||
|
|
||||||
// // export default record
|
|
||||||
// export default function (payload: Payload, helpers: Helpers) {
|
|
||||||
// helpers.logger.info('WHEEEEEEEEEEEEEEEE (record.ts task executor)')
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
export default record
|
export default record
|
|
@ -15,7 +15,7 @@ interface Payload {
|
||||||
function assertPayload(payload: any): asserts payload is Payload {
|
function assertPayload(payload: any): asserts payload is Payload {
|
||||||
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
|
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
|
||||||
if (typeof payload.url !== "string") throw new Error("invalid url");
|
if (typeof payload.url !== "string") throw new Error("invalid url");
|
||||||
if (typeof payload.discordMessageId !== "string") throw new Error("invalid discordMessageId");
|
if (typeof payload.discordMessageId !== "string") throw new Error(`invalid discordMessageId=${payload.discordMessageId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertEnv() {
|
function assertEnv() {
|
||||||
|
@ -26,8 +26,8 @@ async function createRecordingRecord(payload: Payload, helpers: Helpers): Promis
|
||||||
const { url, discordMessageId } = payload
|
const { url, discordMessageId } = payload
|
||||||
const record = {
|
const record = {
|
||||||
url,
|
url,
|
||||||
discordMessageId,
|
discord_message_id: discordMessageId,
|
||||||
isAborted: false
|
is_aborted: false
|
||||||
}
|
}
|
||||||
const res = await fetch('http://postgrest.futureporn.svc.cluster.local:9000/records', {
|
const res = await fetch('http://postgrest.futureporn.svc.cluster.local:9000/records', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -43,9 +43,11 @@ async function createRecordingRecord(payload: Payload, helpers: Helpers): Promis
|
||||||
const statusText = res.statusText
|
const statusText = res.statusText
|
||||||
throw new Error(`fetch failed to create recording record in database. status=${status}, statusText=${statusText}`)
|
throw new Error(`fetch failed to create recording record in database. status=${status}, statusText=${statusText}`)
|
||||||
}
|
}
|
||||||
helpers.logger.info('res.headers as follows.')
|
helpers.logger.info('res.headers.location as follows.')
|
||||||
helpers.logger.info(res.headers)
|
helpers.logger.info(res.headers.get('location')!)
|
||||||
return res.headers.Location.split('.').at(-1)
|
const id = res.headers.get('location')?.split('.').at(-1)
|
||||||
|
if (!id) throw new Error('id could not be parsed from location header');
|
||||||
|
return parseInt(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startRecording: Task = async function (payload, helpers) {
|
export const startRecording: Task = async function (payload, helpers) {
|
||||||
|
@ -54,7 +56,7 @@ export const startRecording: Task = async function (payload, helpers) {
|
||||||
const recordId = await createRecordingRecord(payload, helpers)
|
const recordId = await createRecordingRecord(payload, helpers)
|
||||||
const { url } = payload;
|
const { url } = payload;
|
||||||
console.log(`@todo simulated start_recording with url=${url}, recordId=${recordId}`)
|
console.log(`@todo simulated start_recording with url=${url}, recordId=${recordId}`)
|
||||||
await helpers.addJob('record', { url, recordId })
|
await helpers.addJob('record', { url, recordId }, { maxAttempts: 3 })
|
||||||
}
|
}
|
||||||
|
|
||||||
export default startRecording
|
export default startRecording
|
|
@ -27,7 +27,7 @@ async function handleMessage({ workerUtils, email, msg }: { workerUtils: WorkerU
|
||||||
|
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
console.log(' ✏️✏️ adding process_notif_email job to queue')
|
console.log(' ✏️✏️ adding process_notif_email job to queue')
|
||||||
workerUtils.addJob('process_notif_email', { isMatch, url, platform, channel, displayName, date, userId, avatar })
|
workerUtils.addJob('process_notif_email', { isMatch, url, platform, channel, displayName, date, userId, avatar }, { maxAttempts: 3 })
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(' ✏️ archiving e-mail')
|
console.log(' ✏️ archiving e-mail')
|
||||||
|
@ -50,8 +50,8 @@ async function main() {
|
||||||
})
|
})
|
||||||
|
|
||||||
// demonstrate that we are connected @todo remove this
|
// demonstrate that we are connected @todo remove this
|
||||||
workerUtils.addJob('hello', { name: 'worker' })
|
workerUtils.addJob('hello', { name: 'worker' }, { maxAttempts: 3 })
|
||||||
workerUtils.addJob('identify_image_color', { url: 'https://futureporn-b2.b-cdn.net/ti8ht9bgwj6k783j7hglfg8j_projektmelody-chaturbate-2024-07-18.png' })
|
workerUtils.addJob('identify_image_color', { url: 'https://futureporn-b2.b-cdn.net/ti8ht9bgwj6k783j7hglfg8j_projektmelody-chaturbate-2024-07-18.png' }, { maxAttempts: 3 })
|
||||||
|
|
||||||
// connect to IMAP inbox and wait for new e-mails
|
// connect to IMAP inbox and wait for new e-mails
|
||||||
const email = new Email()
|
const email = new Email()
|
||||||
|
|
|
@ -3,7 +3,9 @@ CREATE TABLE api.records (
|
||||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
discord_message_id TEXT NOT NULL,
|
discord_message_id TEXT NOT NULL,
|
||||||
is_aborted BOOLEAN DEFAULT FALSE
|
is_aborted BOOLEAN DEFAULT FALSE,
|
||||||
|
recording_state TEXT NOT NULL,
|
||||||
|
file_size BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- roles & permissions for our backend automation user
|
-- roles & permissions for our backend automation user
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ALTER TABLE api.records
|
||||||
|
ADD COLUMN recording_state TEXT NOT NULL DEFAULT 'pending' CHECK(recording_state IN ('pending', 'recording', 'aborted', 'ended')),
|
||||||
|
ADD COLUMN file_size BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Grant permissions to the new columns for our backend automation user
|
||||||
|
GRANT UPDATE (recording_state, file_size) ON api.records TO automation;
|
||||||
|
|
||||||
|
-- Grant read-only access to the new columns for our web anon user
|
||||||
|
GRANT SELECT (recording_state, file_size) ON api.records TO web_anon;
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@futureporn/migrations",
|
"name": "@futureporn/migrations",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in New Issue