add api server, handle multiple rooms

This commit is contained in:
cj@futureporn.net 2023-12-06 15:04:22 -08:00
parent 3500bd68ba
commit ebec893656
11 changed files with 903 additions and 308 deletions

287
index.ts
View File

@ -1,20 +1,23 @@
import Room from './src/Room.js'
import AblyWrapper from './src/AblyWrapper.js'
import { loggerFactory } from "./src/logger.js"
import { assertYtdlpExistence } from './src/ytdlp.js'
import * as faye from './src/faye.js'
import * as sound from './src/sound.js'
import * as cb from './src/cb.js'
import { appEnv, getAppContext } from './src/appContext.js'
import { getPushServiceAuth } from './src/headless.js'
// import { getSuperRealtimeClient } from './src/realtime.js'
import {
onCbMessage,
onCbTitle,
import Room from './src/Room.js';
import { loggerFactory } from "./src/logger.js";
import { assertYtdlpExistence } from './src/ytdlp.js';
import * as faye from './src/faye.js';
import * as sound from './src/sound.js';
import * as cb from './src/cb.js';
import { IAppContext, appEnv, getAppContext } from './src/appContext.js';
import { getSuperRealtimeClient } from './src/realtime.js';
import * as Ably from 'ably/promises.js';
import express, { Express, Request, Response } from 'express';
import { z } from 'zod'
import {
onCbMessage,
onCbTitle,
onCbSilence,
onCbStatus,
onCbNotice,
onCbTip,
onCbPassword,
} from './src/cbCallbacks.js'
@ -28,6 +31,14 @@ import { hideBin } from 'yargs/helpers'
import $fastq from 'fastq';
import gotClient from './src/gotClient.js'
import { signalStart } from './src/faye.js'
import { getPushServiceAuth } from './src/headless.js'
interface IRoomRecord {
name: string;
id: string;
monitor: number;
}
const fastq = $fastq.promise
@ -55,7 +66,7 @@ async function getDataDir() {
async function init () {
async function init() {
const dataDir = await getDataDir()
const dbPath = path.join(dataDir, 'scout.db')
@ -76,133 +87,173 @@ async function init () {
*
* listen to room status messages 24/7
* when events are received on the ably realtime client, we log the appropriate message to db.
*
* @deprecated
*/
async function registerRoomStatusListeners(appContext, rooms, cb, ably) {
async function registerRoomStatusListeners(appContext, room) {
const sampleRoom = rooms.at(0)
console.log(sampleRoom)
console.log(`>> registering ${room.name}`);
await Promise.allSettled((rooms.map((r) => r.id)))
await Promise.allSettled((rooms.map((r) => r.dossier)))
const psa = await getPushServiceAuth(sampleRoom.url, permissionForm, cookieString)
const ablyWrapper = new AblyWrapper(appContext, {
chaturbateAuth: cb,
// pushServiceAuth: psa // @todo-- this line has no effect, see below
})
// pass the ablyWrapper instance to the multiple rooms
// this is so rooms instances can access ably realtime client
// for subscriptions.
// multiple subscriptions on the single ablyWrapper instance
// means reduced requests to Ably (good neighbor philosophy)
for (let room of rooms) {
room.attachAblyWrapper(ablyWrapper)
}
const realtimeClient = await ablyWrapper.getRealtimeClient(sampleRoom.url, permissionForm, cookieString)
const channels = psa.channels
const failures = psa?.failures
console.log(`channels and failures as follows`)
console.log(channels)
console.log(failures)
// save channel map to room instance.
// we later use this channelMap to
// subscribe to ably realtime channels.
for (let room of rooms) {
room.attachChannelMap(channels)
}
// handle errors such as passworded rooms
// we use the errors to update the Room instance
// the Room instance is the model in (MVC) methodology
// the model is later used by controller/view to update UI, etc.
Object.entries(failures).forEach(([topic, err]) => {
const id = Room.getIdFromTopic(topic)
const matchingRoom = rooms.find((r) => r.id === id)
matchingRoom.handleError(err)
})
await Promise.allSettled([room.id, room.dossier]);
const psa = await getPushServiceAuth(room.url);
const channelMap = Room.getChannelMapFromPsa(psa);
const realtimeClient = await getSuperRealtimeClient(room.name, psa);
console.log(realtimeClient);
realtimeClient.connection.once('connected', () => {
console.log({ level: 'info', message: 'CB Realtime Connected!' })
console.log(realtimeClient.connection.state)
appContext.logger.log({ level: 'info', message: `${room.name} CB Realtime Connected!` });
appContext.logger.log({ level: 'debug', message: `connection.state=${realtimeClient.connection.state}` });
// subscribe to status channels
const publicRooms = rooms.filter((r) => r.pubOrPass === 'PUBLIC')
console.log('lets subscribe to public room status channels')
const realtimeChannelName = Room.getRealtimeChannelNameFromCbChannelName(channelMap, `RoomStatusTopic#RoomStatusTopic:${room.id}`);
const realtimeChannel = realtimeClient.channels.get(realtimeChannelName);
for (const room of publicRooms) {
room.monitorChannel(room.getAblyChannelName('status'), room.onStatus)
// console.log(channels);
// @see https://github.com/futureporn/futureporn-scout/issues/3
// @todo these are for short-term forcing of, 'we need to get up and running' mandate
// these subscriptions need to be removed from here and added on-demand when
// a room goes live or when admin (me) manually monitors them via api
// room.monitorChannel(room.getAblyChannelName('title'), room.onTitle)
// room.monitorChannel(room.getAblyChannelName('tip'), room.onTip)
// const realtimeChannelName = ``;
// const realtimeChannelName = channels[`RoomStatusTopic#RoomStatusTopic:${room.id}`];
// appContext.logger.log({ level: 'info', message: `>> subscribing to ${realtimeChannelName}` });
}
realtimeChannel.subscribe((message) => {
appContext.logger.log({ level: 'info', message: `got a message from realtime, as follows.` });
console.log(message);
room.onStatus(message);
});
// for (const room of publicRooms) {
// room.monitorChannel(room.getAblyChannelName('status'), room.onStatus)
// // @see https://github.com/futureporn/futureporn-scout/issues/3
// // @todo these are for short-term forcing of, 'we need to get up and running' mandate
// // these subscriptions need to be removed from here and added on-demand when
// // a room goes live or when admin (me) manually monitors them via api
// // room.monitorChannel(room.getAblyChannelName('title'), room.onTitle)
// // room.monitorChannel(room.getAblyChannelName('tip'), room.onTip)
// }
})
await realtimeClient.connect()
realtimeClient.connect();
return realtimeClient;
}
async function record (room: string) {
async function record(room: string) {
const appContext = await init();
appContext.faye = faye.fayeFactory(appContext)
const playlistUrl = await cb.getPlaylistUrl(appContext, room);
signalStart(appContext, room, playlistUrl);
}
async function daemon () {
const appContext = await init()
async function getRoomsFromDb(appContext: IAppContext) {
const stmt = appContext.db.prepare('SELECT name, id FROM rooms WHERE monitor = TRUE')
const roomsRecords = stmt.all()
const rooms = roomsRecords.map((r) => new Room(appContext, {
name: r.name,
id: r.id,
onStatus: (m) => onCbStatus(appContext, r.name, m),
onMessage: (m) => onCbMessage(appContext, r.name, m),
onSilence: (m) => onCbSilence(appContext, r.name, m),
onTitle: (m) => onCbTitle(appContext, r.name, m),
onTip: (m) => onCbTip(appContext, r.name, m),
onPassword: (m) => onCbPassword(appContext, r.name, m),
}))
appContext.faye = faye.fayeFactory(appContext)
const chaturbateAuth = new ChaturbateAuth(appContext)
const ably = new AblyWrapper(appContext, {
chaturbateAuth
const roomsRecords = stmt.all();
return roomsRecords;
}
async function serveApi(appContext: IAppContext) {
appContext.expressApp.get('/', function (req: Request, res: Response) {
res.send('*futureporn-scout pisses on the floor*');
});
appContext.expressApp.get('/rooms', function (req: Request, res: Response) {
res.status(200).json({ rooms: Object.entries(appContext.rooms).map((entry) => entry[0]) });
});
appContext.expressApp.post('/rooms', function (req: Request, res: Response) {
const name = req.query.name as string;
try {
Room.roomNameSchema.parse(name);
} catch (e) {
return res.status(400).json({ code: 400, error: true, message: 'name must be sent as a query param' });
}
try {
appContext.logger.log({ level: 'debug', message: `Creating room name=${name}` });
createRoom(appContext, name)
} catch (e) {
return res.status(500).json({ code: 500, error: true, message: `failed to create ${name}. ${e}` });
}
res.send(`${name} added to watchlist.`);
})
appContext.expressApp.delete('/rooms', function (req: Request, res: Response) {
const name = req.query.name as string;
try {
Room.roomNameSchema.parse(name);
} catch (e) {
return res.status(400).json({ code: 400, error: true, message: 'name must be sent as a query param' });
}
try {
deleteRoom(appContext, name);
return res.send(`${name} deleted from watchlist.`);
} catch (e) {
return res.status(500).json({ code: 500, error: true, message: `failed to delete. ${e}` });
}
})
const port = process.env.PORT || 3030;
appContext.expressApp.listen(port);
appContext.logger.log({ level: 'info', message: `REST API server listening on port ${port}` });
}
// for each room we are scouting, get an Ably realtime client
// once we have an Ably realtime client,
// we attach our custom callback functions which log events to db
for (const room of rooms) {
}
// await registerRoomStatusListeners(appContext, rooms, chaturbateAuth, ably)
function createRoom (appContext: IAppContext, name: string): Room {
if (!!appContext.rooms[name]) {
appContext.logger.log({ level: 'info', message: `${name} is already being watched.` });
return;
}
appContext.logger.log({ level: 'info', message: `creating room ${name}`});
const room = new Room(appContext, {
name: name,
id: null,
onStatus: (m: Ably.Types.Message) => onCbStatus(appContext, name, m),
onMessage: (m: Ably.Types.Message) => onCbMessage(appContext, name, m),
onSilence: (m: Ably.Types.Message) => onCbSilence(appContext, name, m),
onTitle: (m: Ably.Types.Message) => onCbTitle(appContext, name, m),
onTip: (m: Ably.Types.Message) => onCbTip(appContext, name, m),
onPassword: (m: Ably.Types.Message) => onCbPassword(appContext, name, m),
onNotice: (m: Ably.Types.Message) => onCbNotice(appContext, name, m),
})
appContext.rooms[name] = room;
room.startWatching();
return room;
}
async function deleteRoom (appContext: IAppContext, roomName: string): Promise<void> {
const existingRoom = appContext.rooms[roomName];
if (!existingRoom) appContext.logger.log({ level: 'warn', message: `Cannot delete ${roomName} because it is not in the watchlist.` });
const room = appContext.rooms[roomName]
// console.log('here is the room')
// console.log(room)
await room.stopWatching();
delete appContext.rooms[roomName];
}
async function daemon() {
const appContext: IAppContext = await init();
appContext.faye = faye.fayeFactory(appContext);
const roomsRecords = await getRoomsFromDb(appContext);
appContext.logger.log({ level: 'info', message: `Watching the following rooms` });
for (const room of roomsRecords) {
appContext.logger.log({ level: 'info', message: `${room.name}` });
createRoom(appContext, room.name);
}
// for (const room of Object.entries(appContext.rooms)) {
// const r = room[1];
// r.startWatching();
// }
serveApi(appContext);
}
/**
* get the room's dossier
*/
async function dossier (roomName: string) {
async function dossier(roomName: string) {
const appContext = await init()
const r = new Room(appContext, {
name: roomName
@ -218,7 +269,7 @@ async function dossier (roomName: string) {
* export chat logs into a format suitable for publishing
* @todo
*/
async function exportLogs (options) {
async function exportLogs(options) {
const appContext = await init()
// let stmt = appContext.db.prepare('SELECT ...')
@ -247,8 +298,8 @@ yargs(hideBin(process.argv))
.command({
command: 'daemon',
alias: 'd',
desc: 'Listen & log chaturbate events',
builder: () => {},
desc: 'Listen & log chaturbate events',
builder: () => { },
handler: (argv) => {
console.info(argv)
daemon()
@ -314,11 +365,11 @@ yargs(hideBin(process.argv))
default: now,
required: false
})
// .option('auto', {
// alias: 'a',
// describe: 'gets the most recent stream',
// required: false
// })
// .option('auto', {
// alias: 'a',
// describe: 'gets the most recent stream',
// required: false
// })
},
handler: (argv) => {
if (argv.auto && (argv.since !== epoch || argv.until !== now)) {

View File

@ -1,6 +1,6 @@
{
"name": "futureporn-scout",
"version": "1.0.4",
"version": "2.0.0",
"description": "event emitter that detects start and end of stream",
"main": "index.js",
"license": "Unlicense",
@ -16,6 +16,7 @@
"type": "module",
"dependencies": {
"@playwright/test": "^1.40.1",
"@types/express": "^4.17.21",
"ably": "1.2.37",
"better-sqlite3": "^8.7.0",
"cheerio": "1.0.0-rc.12",
@ -24,6 +25,7 @@
"dexie-export-import": "^4.0.7",
"dotenv": "^16.3.1",
"execa": "^7.2.0",
"express": "^4.18.2",
"fastify": "^4.24.3",
"fastq": "^1.15.0",
"faye": "^1.4.0",

View File

@ -8,6 +8,9 @@ dependencies:
'@playwright/test':
specifier: ^1.40.1
version: 1.40.1
'@types/express':
specifier: ^4.17.21
version: 4.17.21
ably:
specifier: 1.2.37
version: 1.2.37
@ -32,6 +35,9 @@ dependencies:
execa:
specifier: ^7.2.0
version: 7.2.0
express:
specifier: ^4.18.2
version: 4.18.2
fastify:
specifier: ^4.24.3
version: 4.24.3
@ -494,6 +500,13 @@ packages:
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
dev: false
/@types/body-parser@1.19.5:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies:
'@types/connect': 3.4.38
'@types/node': 20.10.3
dev: false
/@types/cacheable-request@6.0.3:
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
dependencies:
@ -503,22 +516,58 @@ packages:
'@types/responselike': 1.0.3
dev: false
/@types/connect@3.4.38:
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
dependencies:
'@types/node': 20.10.3
dev: false
/@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
'@types/ms': 0.7.34
dev: false
/@types/express-serve-static-core@4.17.41:
resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
dependencies:
'@types/node': 20.10.3
'@types/qs': 6.9.10
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
dev: false
/@types/express@4.17.21:
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
dependencies:
'@types/body-parser': 1.19.5
'@types/express-serve-static-core': 4.17.41
'@types/qs': 6.9.10
'@types/serve-static': 1.15.5
dev: false
/@types/http-cache-semantics@4.0.4:
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
dev: false
/@types/http-errors@2.0.4:
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
dev: false
/@types/keyv@3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
'@types/node': 20.10.3
dev: false
/@types/mime@1.3.5:
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
dev: false
/@types/mime@3.0.4:
resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==}
dev: false
/@types/mocha@10.0.6:
resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==}
dev: true
@ -533,6 +582,14 @@ packages:
undici-types: 5.26.5
dev: false
/@types/qs@6.9.10:
resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==}
dev: false
/@types/range-parser@1.2.7:
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
dev: false
/@types/responselike@1.0.3:
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
dependencies:
@ -543,6 +600,21 @@ packages:
resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==}
dev: false
/@types/send@0.17.4:
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
dependencies:
'@types/mime': 1.3.5
'@types/node': 20.10.3
dev: false
/@types/serve-static@1.15.5:
resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==}
dependencies:
'@types/http-errors': 2.0.4
'@types/mime': 3.0.4
'@types/node': 20.10.3
dev: false
/@types/triple-beam@1.3.5:
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
dev: false
@ -594,6 +666,14 @@ packages:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
dev: false
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
dev: false
/acorn-walk@8.3.0:
resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==}
engines: {node: '>=0.4.0'}
@ -727,6 +807,10 @@ packages:
is-array-buffer: 3.0.2
dev: false
/array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@ -879,6 +963,26 @@ packages:
resolution: {integrity: sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==}
dev: false
/body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.1
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: false
@ -939,6 +1043,11 @@ packages:
ieee754: 1.2.1
dev: false
/bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: false
/cacheable-lookup@5.0.4:
resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==}
engines: {node: '>=10.6.0'}
@ -1274,6 +1383,18 @@ packages:
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
/content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: false
/content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
dev: false
/continuation-local-storage@3.2.1:
resolution: {integrity: sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==}
dependencies:
@ -1281,6 +1402,10 @@ packages:
emitter-listener: 1.1.2
dev: false
/cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: false
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
@ -1403,6 +1528,17 @@ packages:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
dev: false
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
dev: false
/debug@3.2.7(supports-color@5.5.0):
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@ -1503,6 +1639,16 @@ packages:
esprima: 4.0.1
dev: false
/depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dev: false
/destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
@ -1578,6 +1724,10 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
/ejs@3.1.9:
resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==}
engines: {node: '>=0.10.0'}
@ -1603,6 +1753,11 @@ packages:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
dev: false
/encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
dev: false
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
@ -1700,6 +1855,10 @@ packages:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@ -1742,6 +1901,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
dev: false
/event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
@ -1784,6 +1948,45 @@ packages:
engines: {node: '>=6'}
dev: false
/express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.1
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.5.0
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.2.0
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.1
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.7
proxy-addr: 2.0.7
qs: 6.11.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.18.0
serve-static: 1.15.0
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
@ -1957,6 +2160,21 @@ packages:
dependencies:
to-regex-range: 5.0.1
/finalhandler@1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
engines: {node: '>= 0.8'}
dependencies:
debug: 2.6.9
encodeurl: 1.0.2
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/find-my-way@7.7.0:
resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==}
engines: {node: '>=14'}
@ -2051,6 +2269,11 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: false
/fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
dev: false
@ -2361,6 +2584,17 @@ packages:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
dev: false
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
dev: false
/http-parser-js@0.5.8:
resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==}
dev: false
@ -2934,6 +3168,11 @@ packages:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: false
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: false
/merge-deep@3.0.3:
resolution: {integrity: sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==}
engines: {node: '>=0.10.0'}
@ -2943,6 +3182,10 @@ packages:
kind-of: 3.2.2
dev: false
/merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: false
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: false
@ -2952,6 +3195,11 @@ packages:
engines: {node: '>= 8'}
dev: true
/methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: false
/micromatch@4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
@ -2960,6 +3208,24 @@ packages:
picomatch: 2.3.1
dev: true
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
dev: false
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@ -3079,6 +3345,10 @@ packages:
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
dev: false
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@ -3128,6 +3398,11 @@ packages:
- supports-color
dev: false
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: false
/netmask@2.0.2:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
@ -3296,6 +3571,13 @@ packages:
engines: {node: '>=14.0.0'}
dev: false
/on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@ -3451,6 +3733,11 @@ packages:
entities: 4.5.0
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: false
/password-prompt@1.1.3:
resolution: {integrity: sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==}
dependencies:
@ -3487,6 +3774,10 @@ packages:
minipass: 7.0.4
dev: true
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
dev: false
/path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@ -3912,6 +4203,13 @@ packages:
- utf-8-validate
dev: false
/qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: false
/qs@6.11.2:
resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
engines: {node: '>=0.6'}
@ -3946,6 +4244,21 @@ packages:
safe-buffer: 5.2.1
dev: true
/range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: false
/raw-body@2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
@ -4197,6 +4510,27 @@ packages:
lru-cache: 6.0.0
dev: false
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/sequin@0.1.1:
resolution: {integrity: sha512-hJWMZRwP75ocoBM+1/YaCsvS0j5MTPeBHJkS2/wruehl9xwtX30HlDF1Gt6UZ8HHHY8SJa2/IL+jo+JJCd59rA==}
engines: {node: '>=0.4.0'}
@ -4208,6 +4542,18 @@ packages:
randombytes: 2.1.0
dev: true
/serve-static@1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
engines: {node: '>= 0.8.0'}
dependencies:
encodeurl: 1.0.2
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
transitivePeerDependencies:
- supports-color
dev: false
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: false
@ -4239,6 +4585,10 @@ packages:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
dev: false
/setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
/seventh@0.9.2:
resolution: {integrity: sha512-C+dnbBXIEycnrN6/CpFt/Rt8ccMzAX3wbwJU61RTfC8lYPMzSkKkAVWnUEMTZDHdvtlrTupZeCUK4G+uP4TmRQ==}
engines: {node: '>=16.13.0'}
@ -4401,6 +4751,11 @@ packages:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
dev: false
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/stream-transform@2.1.3:
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
dependencies:
@ -4640,6 +4995,11 @@ packages:
engines: {node: '>=12'}
dev: false
/toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: false
/touch@3.1.0:
resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==}
hasBin: true
@ -4847,6 +5207,14 @@ packages:
engines: {node: '>=10'}
dev: true
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
dev: false
/typed-array-buffer@1.0.0:
resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
engines: {node: '>= 0.4'}
@ -4933,6 +5301,11 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
/unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
dev: false
/uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
@ -4949,6 +5322,11 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
/utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
dev: false
/uuid@3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
@ -4959,6 +5337,11 @@ packages:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: false
/vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: false
/vizion@2.2.1:
resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==}
engines: {node: '>=4.0'}

View File

@ -3,14 +3,16 @@ import {
FormData
} from 'formdata-polyfill/esm.min.js'
import cheerio from 'cheerio'
import ChaturbateAuth from './ChaturbateAuth'
import * as Ably from 'ably/promises.js';
import ChaturbateAuth from './ChaturbateAuth.js'
import { Logger } from 'winston'
import { IAppContext } from './appContext'
import AblyWrapper from './AblyWrapper'
import { IAppContext } from './appContext.js';
import { IChannelMap, IPushServiceAuth, getPushServiceAuth } from './headless.js';
import { getSuperRealtimeClient } from './realtime.js';
import { z } from 'zod';
export interface IRoomOptions {
chaturbateAuth?: any;
ablyWrapper?: any;
name: string;
onStatus?: any;
onMessage?: any;
@ -18,6 +20,7 @@ export interface IRoomOptions {
onTitle?: any;
onTip?: any;
onPassword?: any;
onNotice?: any;
id?: string;
}
@ -46,58 +49,60 @@ interface Topics {
[topicId: string]: RoomTopic;
}
export interface IRoomMessage {
name: string;
id: string;
encoding: null;
data: {
tid: string;
ts: number;
_topic: string;
message: string;
font_family: string;
font_color: string;
background: string;
id: string;
from_user: {
username: string;
gender: string;
is_broadcaster: boolean;
in_fanclub: boolean;
is_following: boolean;
is_mod: boolean;
has_tokens: boolean;
tipped_recently: boolean;
tipped_alot_recently: boolean;
tipped_tons_recently: boolean;
};
status?: string;
method: string;
pub_ts: number;
};
}
// export interface IRoomMessage {
// name: string;
// id: string;
// encoding: null;
// data: {
// tid: string;
// ts: number;
// _topic: string;
// message: string;
// font_family: string;
// font_color: string;
// background: string;
// id: string;
// from_user: {
// username: string;
// gender: string;
// is_broadcaster: boolean;
// in_fanclub: boolean;
// is_following: boolean;
// is_mod: boolean;
// has_tokens: boolean;
// tipped_recently: boolean;
// tipped_alot_recently: boolean;
// tipped_tons_recently: boolean;
// };
// status?: string;
// method: string;
// pub_ts: number;
// };
// }
export default class Room {
private id: string | Promise<string>;
public id: string | Promise<string>;
private logger: Logger;
private appContext: IAppContext;
private chaturbateAuth: ChaturbateAuth;
private ablyWrapper: AblyWrapper;
private name: string;
private url: string;
public name: string;
public url: string;
private realtimeClient: Ably.Realtime | null;
private csrfToken: string | null;
private tokenRequest: any;
private ablyTokenRequest: any;
private realtimeHost: string | null;
private fallbackHosts: string[] | null;
private pushServiceAuth: any;
private onStatus: ((message: IRoomMessage) => void);
private onMessage: ((message: any) => void);
private onSilence: ((message: any) => void);
private onTitle: ((message: any) => void);
private onTip: ((message: any) => void);
private onPassword: ((message: any) => void);
private onStatus: ((message: Ably.Types.Message) => void);
private onMessage: ((message: Ably.Types.Message) => void);
private onSilence: ((message: Ably.Types.Message) => void);
private onTitle: ((message: Ably.Types.Message) => void);
private onTip: ((message: Ably.Types.Message) => void);
private onPassword: ((message: Ably.Types.Message) => void);
private onNotice: ((message: Ably.Types.Message) => void);
private dossier: any;
public errors: string[];
public pubOrPass: PubOrPass;
@ -107,16 +112,17 @@ export default class Room {
constructor(appContext: IAppContext, opts: IRoomOptions) {
if (opts?.name === undefined) throw new Error('Room constructor requires a room name.')
Room.roomNameSchema.parse(opts.name);
this.logger = appContext.logger // this needs to be defined before this.id
this.appContext = appContext
this.chaturbateAuth = opts.chaturbateAuth;
this.ablyWrapper = opts.ablyWrapper;
this.name = opts.name
this.url = 'https://chaturbate.com/' + this.name + '/'
this.csrfToken = null
this.tokenRequest = null
this.ablyTokenRequest = null
this.realtimeHost = null
this.realtimeClient = null
this.fallbackHosts = null
this.pushServiceAuth = null
this.onStatus = opts.onStatus;
@ -125,6 +131,7 @@ export default class Room {
this.onTitle = opts.onTitle;
this.onTip = opts.onTip;
this.onPassword = opts.onPassword;
this.onNotice = opts.onNotice;
this.dossier = null
this.errors = [];
this.pubOrPass = PubOrPass.Public
@ -134,28 +141,39 @@ export default class Room {
// We put this last because they depend on other opts
// before making a network request
this.id = opts?.id || this.getRoomId()
this.id = opts?.id || this.getRoomId()
}
handleError (err: string) {
this.logger.log({ level: 'debug', message: `handling room error. ${err}`})
handleError(err: string) {
this.logger.log({ level: 'debug', message: `handling room error. ${err}` })
this.errors.push(err)
if (/room has a password/.test(err)) this.pubOrPass = PubOrPass.Password
}
// depends on initialRoomDossier existing, which is missing when room is passworded
// will return null if dossier is missing.
async getRoomId (): Promise<string | null> {
async getRoomId(): Promise<string | null> {
try {
const dossier = await this.getInitialRoomDossier()
return this.id = dossier.room_uid
} catch (e) {
this.logger.log({ level: 'warning', message: `error while getting room ID. ${e}` })
this.logger.log({ level: 'warn', message: `error while getting room ID. ${e}` })
return this.id = null
}
}
async getLocalPushServiceAuth(): Promise<IPushServiceAuth> {
if (!!this.pushServiceAuth) return this.pushServiceAuth;
try {
const psa = await getPushServiceAuth(this.url);
this.channelMap = psa.channels;
return this.pushServiceAuth = psa;
} catch (e) {
this.logger.log({ level: 'warn', message: `error while getting psa. ${e}` })
}
}
async getInitialRoomDossier(): Promise<any | null> {
try {
@ -170,11 +188,14 @@ export default class Room {
return dossier;
} catch (error) {
// Handle the error gracefully
this.logger.log({ level: 'warning', message: `Error fetching initial room dossier: ${error.message}` });
this.logger.log({ level: 'warn', message: `Error fetching initial room dossier: ${error.message}` });
return null; // Or any other appropriate action you want to take
}
}
static roomNameSchema = z.string().regex(/^[a-zA-Z0-9_]+$/);
static getIdFromTopic(topic: string): string {
const parts = topic.split(':');
if (/^\d+$/.test(parts.at(-1))) {
@ -213,26 +234,26 @@ export default class Room {
*/
static mergeChannelsRequests(requests: ChannelsRequest[]): ChannelsRequest {
const mergedRequest: ChannelsRequest = {};
for (const request of requests) {
for (const topic in request) {
if (!mergedRequest.hasOwnProperty(topic)) {
mergedRequest[topic] = {};
}
if (request[topic].broadcaster_uid) {
mergedRequest[topic].broadcaster_uid = request[topic].broadcaster_uid;
}
if (request[topic].user_uid) {
mergedRequest[topic].user_uid = request[topic].user_uid;
}
}
}
return mergedRequest;
}
static formalizeChannelsRequest(channelsRequest: ChannelsRequest, csrfToken: string): FormData {
@ -268,7 +289,7 @@ export default class Room {
crForm[`RoomStatusTopic#RoomStatusTopic:${roomId}`] = { "broadcaster_uid": roomId }
// crForm[`RoomTitleChangeTopic#RoomTitleChangeTopic:${roomId}`] = { "broadcaster_uid": roomId }
crForm[`RoomTipAlertTopic#RoomTipAlertTopic:${roomId}`] = { "broadcaster_uid": roomId }
crForm[`RoomPasswordProtectedTopic#RoomPasswordProtectedTopic:${roomId}`] = { "broadcaster_uid": roomId}
crForm[`RoomPasswordProtectedTopic#RoomPasswordProtectedTopic:${roomId}`] = { "broadcaster_uid": roomId }
this.channelsRequest = crForm
return this.channelsRequest
}
@ -287,7 +308,7 @@ export default class Room {
// if (topics.includes('status')) {
// crForm[`RoomStatusTopic#RoomStatusTopic:${this.id}`] = { "broadcaster_uid":this.id }
// }
// if (topics.includes('title')) {
// crForm[`RoomTitleChangeTopic#RoomTitleChangeTopic:${this.id}`] = { "broadcaster_uid": this.id }
// }
@ -301,7 +322,7 @@ export default class Room {
// }
// return this.channelsRequest = crForm
// }
// the number suffixes are assumed to be session ids.
// opening multiple tabs of the same room will not increment the :n suffix.
// however, opening different rooms in multiple tags will increment the :n suffix
@ -311,28 +332,28 @@ export default class Room {
if (!roomIds || roomIds.length === 0) {
throw new Error('roomIds is required but it was undefined or empty');
}
const generateTopic = (topicKey: string, topicValue: string) => {
return JSON.stringify({ [topicKey]: topicValue });
};
let form = new FormData();
let formTopics = {};
roomIds.forEach((roomId) => {
if (topics.includes('message')) {
formTopics[`RoomMessageTopic#RoomMessageTopic:${roomId}`] = { "broadcaster_uid": roomId }
}
if (topics.includes('silence')) {
formTopics[`RoomSilenceTopic#RoomSilenceTopic:${roomId}`] = { "broadcaster_uid":roomId }
formTopics[`RoomSilenceTopic#RoomSilenceTopic:${roomId}`] = { "broadcaster_uid": roomId }
}
if (topics.includes('status')) {
formTopics[`RoomStatusTopic#RoomStatusTopic:${roomId}`] = { "broadcaster_uid":roomId }
formTopics[`RoomStatusTopic#RoomStatusTopic:${roomId}`] = { "broadcaster_uid": roomId }
}
if (topics.includes('title')) {
formTopics[`RoomTitleChangeTopic#RoomTitleChangeTopic:${roomId}`] = { "broadcaster_uid": roomId }
}
@ -342,7 +363,7 @@ export default class Room {
}
if (topics.includes('password')) {
formTopics[`RoomPasswordProtectedTopic#RoomPasswordProtectedTopic:${roomId}`] = { "broadcaster_uid": roomId}
formTopics[`RoomPasswordProtectedTopic#RoomPasswordProtectedTopic:${roomId}`] = { "broadcaster_uid": roomId }
}
});
@ -352,32 +373,129 @@ export default class Room {
}
/**
* If this.ablyWrapper is not defined at room instantiation, it can be attached later.
* this is used to manage ably realtime client subscriptions.
*/
attachAblyWrapper(ablyWrapper: AblyWrapper): void {
this.ablyWrapper = ablyWrapper;
}
attachChannelMap(channelMap: ChannelMap): void {
this.channelMap = channelMap
}
/**
* Get the ably channel string from memory.
* we can pass in a generic topic such as 'status'
*
*/
getAblyChannelName(genericTopic: string): string {
// get the cb topic string
// use the cb topic string to find ably ch name in map
const cbTopic = this.getChaturbateTopicString(genericTopic)
return this.channelMap[cbTopic]
// static getAblyChannelName(channelMap, genericTopic: string): string {
// // get the cb topic string
// // use the cb topic string to find ably ch name in map
// const cbTopic = this.getChaturbateTopicString(genericTopic)
// return this.channelMap[cbTopic]
// }
static getChannelMapFromPsa(psa: IPushServiceAuth) {
return psa.channels;
}
static getRealtimeChannelNameFromCbChannelName(channelMap: ChannelMap, cbChannelName: string) {
return channelMap[cbChannelName];
}
/**
*
* getChaturbateTopicString
*
* Chaturbate now groups message together. the topics that are grouped are
*
* * status, message, password
* * title, silence
*
* @see https://gitea.futureporn.net/futureporn/futureporn-scout/issues/3
*
* @param genericTopic
* @returns
*/
getChaturbateTopicString(genericTopic: string): string {
let ch: string;
if (genericTopic === 'status' || genericTopic === 'message' || genericTopic === 'password') {
ch = `RoomStatusTopic#RoomStatusTopic:${this.id}`;
}
else if (genericTopic === 'tip') {
ch = `RoomTipAlertTopic#RoomTipAlertTopic:${this.id}`;
}
else if (genericTopic === 'title' || genericTopic === 'silence') {
ch = `RoomTitleChangeTopic#RoomTitleChangeTopic:${this.id}`;
}
return ch;
}
async getRealtimeClient(): Promise<Ably.Realtime> {
if (!!this.realtimeClient) return this.realtimeClient;
try {
const psa = await this.getLocalPushServiceAuth();
const rtc = await getSuperRealtimeClient(this.name, psa);
return this.realtimeClient = rtc;
} catch (e) {
this.logger.log({ level: 'error', message: `failed to get realtimeClient. ${e}` });
}
}
async handleRealtimeEvent(message: Ably.Types.Message): Promise<void> {
this.appContext.logger.log({ level: 'debug', message: `handling realtime event with _topic=${message?.data?._topic}` });
const cbTopic = message?.data?._topic;
if (cbTopic.startsWith('RoomStatusTopic')) {
this.appContext.logger.log({ level: 'warn', message: `⚠️⚠️⚠️ _topic=${cbTopic} ???? is this needed?` })
this.onStatus(message);
} else if (cbTopic.startsWith('RoomMessageTopic')) {
this.onMessage(message);
} else if (cbTopic.startsWith('RoomTipAlertTopic')) {
this.onTip(message);
} else if (cbTopic.startsWith('RoomTitleChangeTopic')) {
this.onTitle(message);
} else if (cbTopic.startsWith('RoomSilenceTopic')) {
this.onSilence(message);
} else if (cbTopic.startsWith('RoomPasswordProtectedTopic')) {
this.onPassword(message);
} else if (cbTopic.startsWith('RoomNoticeTopic')) {
this.onNotice(message);
} else {
this.appContext.logger.log({ level: 'warn', message: `⚠️⚠️⚠️ thure was an incomming message (${cbTopic}) which we dont know how to handle. ` })
this.appContext.logger.log({ level: 'warn', message: message })
}
}
async subscribeToRealtimeChannel(realtimeChannelName: string): Promise<void> {
const realtimeChannel = this.realtimeClient.channels.get(realtimeChannelName);
this.appContext.logger.log({ level: 'debug', message: `subscribing to ${realtimeChannelName}` });
realtimeChannel.subscribe((message) => {
this.appContext.logger.log({ level: 'debug', message: JSON.stringify(message, null, 2) });
this.handleRealtimeEvent(message);
});
}
deduplicateChannelMap(channelMap: ChannelMap): ChannelMap {
const uniqueValuesSet = new Set<string>();
const deduplicatedMap: ChannelMap = {};
for (const topic in channelMap) {
const value = channelMap[topic];
// Check if the value is not already in the uniqueValuesSet
if (!uniqueValuesSet.has(value)) {
// Add the value to the uniqueValuesSet and the deduplicatedMap
uniqueValuesSet.add(value);
deduplicatedMap[topic] = value;
}
}
return deduplicatedMap;
}
/*
{
"channels": {
"RoomTipAlertTopic#RoomTipAlertTopic:VVKHB7L": "room:tip_alert:VVKHB7L",
"RoomPurchaseTopic#RoomPurchaseTopic:VVKHB7L": "room_unsessioned:grouped:VVKHB7L",
@ -399,73 +517,53 @@ export default class Room {
"RoomEnterLeaveTopic#RoomEnterLeaveTopic:VVKHB7L": "room:enter_leave:VVKHB7L",
"GameUpdateTopic#GameUpdateTopic:VVKHB7L": "room_unsessioned:grouped:VVKHB7L"
}
}
*/
async subscribeToEvents(): Promise<void> {
if (!this.channelMap) throw new Error('attempting to subscribeToEvents before we have a channelMap!');
const channelEntries: [string, string][] = Object.entries(this.channelMap);
const interestingChannels = channelEntries.filter(([topic]) => {
return (
topic.startsWith('RoomStatusTopic') ||
topic.startsWith('RoomTipAlertTopic') ||
topic.startsWith('RoomTitleChangeTopic') ||
topic.startsWith('RoomSilenceTopic') ||
topic.startsWith('RoomPasswordProtectedTopic')
)
}).map((ic) => ic[1]);
/**
*
* getChaturbateTopicString
*
* Chaturbate now groups message together. the topics that are grouped are
*
* * status, message, password
* * title, silence
*
* @see https://gitea.futureporn.net/futureporn/futureporn-scout/issues/3
*
* @param genericTopic
* @returns
*/
getChaturbateTopicString(genericTopic: string): string {
let ch: string;
if (genericTopic === 'status' || genericTopic === 'message' || genericTopic === 'password') {
ch = `RoomStatusTopic#RoomStatusTopic:${this.id}`
}
else if (genericTopic === 'tip') {
ch = `RoomTipAlertTopic#RoomTipAlertTopic:${this.id}`
}
else if (genericTopic === 'title' || genericTopic === 'silence') {
ch = `RoomTitleChangeTopic#RoomTitleChangeTopic:${this.id}`
}
return ch
// some of the CB channels map to the same realtime channel name.
// Because of this, we could have duplicates.
// We need to deduplicate.
const deduplicatedChannels = [...new Set(interestingChannels)];
this.appContext.logger.log({ level: 'debug', message: `deduplicatedChannels=${deduplicatedChannels}` })
for (const channel of deduplicatedChannels) {
this.appContext.logger.log({ level: 'debug', message: `subscribing to channel=${channel}` });
this.subscribeToRealtimeChannel(channel);
};
}
/**
* Subscribe to an ably realtime channel and handle events heard
*
* Uses the shared AblyWrapper to minimize network connections.
* Begin watching the CB room events.
*/
async monitorChannel(channelTopicString: string, eventHandler: Function): Promise<void> {
// assert ably realtime connection
if (this.ablyWrapper.realtimeClient.connection.state !== 'connected') {
// await this.realtime.connection.await()
throw new Error('cannot monitor channel because realtime client is not connected.')
}
// subscribe to realtime channel
// attach callbacks to subscription handler
const realtimeChannel = this.ablyWrapper.realtimeClient.channels.get(channelTopicString);
realtimeChannel.subscribe((message) => {
eventHandler(message)
});
async startWatching(): Promise<void> {
const rtc = await this.getRealtimeClient();
rtc.connect();
await rtc.connection.once('connected');
this.logger.log({ level: 'debug', message: `Connected to ${this.name}` });
this.subscribeToEvents();
}
/**
* Unsubscribe from an ably realtime channel
* Cease watching the CB room events.
*/
async dropChannel(channelTopicString: string): Promise<void> {
// assert ably realtime connection(?)
// unsubscribe
// get a handle on the channel in question
async stopWatching(): Promise<void> {
const rtc = await this.getRealtimeClient();
rtc.close();
return;
}
@ -473,4 +571,4 @@ export default class Room {
}
}

View File

@ -1,4 +1,24 @@
import 'dotenv/config';
import express, { Express } from 'express';
import Room from './Room.js';
export interface IRoomMap {
[name: string]: Room;
}
export interface IAppContext {
env: Record<string, string>;
logger: any;
db: any;
roomTimers: {};
sound: any;
cb: any;
dataDir: any;
gotClient: any;
faye: any | null;
expressApp: Express;
rooms: IRoomMap;
}
export const appEnv: string[] = ['PUBSUB_SERVER_URL'];
@ -25,18 +45,7 @@ export function getAppContext(
dataDir,
gotClient,
faye: null,
expressApp: express(),
rooms: {}
};
}
export interface IAppContext {
env: Record<string, string>;
logger: any;
db: any;
roomTimers: {};
sound: any;
cb: any;
dataDir: any;
gotClient: any;
faye: any | null;
}

View File

@ -1,7 +1,6 @@
import cheerio from 'cheerio'
import { execa } from 'execa'
import pRetry from 'p-retry'
import gotClient from './gotClient';
export function getRandomRoomFromCbHtml (body) {
const $ = cheerio.load(body);

View File

@ -1,9 +1,9 @@
// import { createVod } from './strapi.js'
import Stream from './Stream.js'
import { signalStart, sendSignal } from './faye.js'
import { IRoomMessage } from './Room.js'
import { IAppContext } from './appContext.js'
// import { execa } from 'execa'
import * as Ably from 'ably/promises.js';
import { application } from 'express';
export const onCbPassword = async (appContext, roomName, message) => {
appContext.logger.log({ level: 'debug', message: `[ ] room is passworded`})
@ -12,7 +12,7 @@ export const onCbPassword = async (appContext, roomName, message) => {
}
export const onCbTip = async (appContext, roomName, message) => {
appContext.logger.log({ level: 'debug', message: `$$$ [TIP] ${JSON.stringify(message)}` })
appContext.logger.log({ level: 'debug', message: `💲💲💲 [TIP] ${JSON.stringify(message)}` })
const stmt = appContext.db.prepare(`INSERT INTO tips VALUES ($_room, $name, $encoding, $data_tid, $data_ts, $data_amount, $data_message, $data_history, $data_is_anonymous_tip, $data_to_username, $data_from_username, $data_gender, $data_is_broadcaster, $data_in_fanclub, $data_is_following, $data_is_mod, $data_has_tokens, $data_tipped_recently, $data_tipped_alot_recently, $data_tipped_tons_recently, $data_method, $data_pub_ts)`)
stmt.run({
'_room': roomName,
@ -85,6 +85,23 @@ export const onCbStart = async (appContext, roomName, message) => {
// !!appContext.roomTimers[roomName].offline && clearTimeout(appContext.roomTimers[roomName].offline)
}
export const onCbNotice = (appContext: IAppContext, roomName: string, message: Ably.Types.Message) => {
appContext.logger.log({ level: 'debug', message: `📢📢📢 [NOTICE] roomName=${roomName} ${JSON.stringify(message)}` })
const stmt = appContext.db.prepare(`INSERT INTO notices VALUES (
$id,
$_room,
$data_messages,
$data_tid,
$data_ts
)`);
stmt.run({
'id': message?.id || null,
'_room': roomName,
'data_messages': message?.data?.messages.join('\n') || null,
'data_tid': message?.data?.tid || null,
'data_ts': message?.data?.ts || null,
});
}
export const onCbStop = (appContext, roomName, message) => {
appContext.logger.log({ level: 'debug', message: `🛑🛑🛑 [STREAM STOP] ${JSON.stringify(message)}` })
@ -131,15 +148,17 @@ export const onCbStop = (appContext, roomName, message) => {
*
* onCbStatus
*
*
* CB changed their systems sometime around fall 2023 to where mutiple event types are grouped into a single channel. Due to this, a status callback might actually contain a chat message, so we need to test _topic and see what type of event data we are dealing with.
* @see https://gitea.futureporn.net/futureporn/futureporn-scout/issues/3
*
* @param appContext
* @param roomName
* @param message
*
* @deprecated I don't think CB uses RoomStatusTopic anymore.
*/
export const onCbStatus = async (appContext: IAppContext, roomName: string, message: IRoomMessage) => {
export const onCbStatus = async (appContext: IAppContext, roomName: string, message: Ably.Types.Message) => {
if (message.data._topic === 'RoomMessageTopic') {
onCbMessage(appContext, roomName, message)
} else if (message.data?.status === 'public') {
@ -158,7 +177,7 @@ export const onCbStatus = async (appContext: IAppContext, roomName: string, mess
* @param appContext
* @param message
*/
export const createStreamVOD = (appContext: IAppContext, message: IRoomMessage) => {
export const createStreamVOD = (appContext: IAppContext, message: Ably.Types.Message) => {
// find the relevant StreamSegments
// group the StreamSegments into a Stream
// create a vod using the start/stop timestamps of the Stream, and the messages falling within those timestamps
@ -225,7 +244,7 @@ export const onCbSilence = (appContext, roomName, message) => {
export const onCbMessage = (appContext, roomName, message) => {
appContext.logger.log({ level: 'debug', message: `[💬 CHAT MESSAGE] ${JSON.stringify(message)}` })
appContext.logger.log({ level: 'debug', message: `💬💬💬 [CHAT MESSAGE] ${JSON.stringify(message)}` })
const stmt = appContext.db.prepare(`INSERT INTO messages VALUES (
$_room,

View File

@ -4,9 +4,15 @@ import { z } from 'zod';
import { iPushServiceAuthSchema } from './headlessSchema.js';
import qs from 'qs';
export interface IChannelMap {
[topic: string]: string;
}
export interface IPushServiceAuth {
token: string;
channels: Record<string, string>;
channels: IChannelMap;
failures: Record<string, string>;
token_request: {
keyName: string;
@ -101,6 +107,7 @@ export async function getAblyAgentVersionSemver(): Promise<string> {
export async function getPushServiceAuth(roomUrl: string): Promise<IPushServiceAuth> {
if (!roomUrl) throw new Error('roomUrl is a required param, but it was undefined.');
const roomUrlUrl = new URL(roomUrl);
const browser = await puppeteer.launch({
headless: 'new'
@ -126,18 +133,29 @@ export async function getPushServiceAuth(roomUrl: string): Promise<IPushServiceA
resolve(json);
}
})
await page.goto(roomUrlUrl.toString());
console.log(`>> navigating to ${roomUrlUrl.toString()}`)
try {
await page.goto(roomUrlUrl.toString());
} catch (e) {
// It's likely not an issue because we might already have the data we need.
if (!e.message.includes('Navigation failed because browser has disconnected')) {
console.log(`>> caught an error during navigation.`);
console.log(e);
}
}
})
let psa;
try {
psa = iPushServiceAuthSchema.parse(json);
// console.log(psa);
console.log(`>> got psa`);
} catch (e) {
console.log(`>> error!!!! ${e}`);
if (e instanceof Error) {
console.error(e)
}
} finally {
console.log('>> closing the browser')
await browser.close()
}

View File

@ -1,9 +1,11 @@
// Generated by ts-to-zod
import { z } from "zod";
export const iChannelMapSchema = z.record(z.string());
export const iPushServiceAuthSchema = z.object({
token: z.string(),
channels: z.record(z.string()),
channels: iChannelMapSchema,
failures: z.record(z.string()),
token_request: z.object({
keyName: z.string(),

View File

@ -1,39 +1,27 @@
import * as Ably from 'ably/promises';
import { getPushServiceAuth } from './headless';
import { Realtime } from 'ably';
import * as Ably from 'ably/promises.js';
import { IPushServiceAuth, getPushServiceAuth } from './headless.js';
import { iPushServiceAuthSchema } from './headlessSchema.js';
export async function getSuperRealtimeClient(room: string): Promise<Ably.Realtime> {
export async function getSuperRealtimeClient(room: string, psa: IPushServiceAuth): Promise<Ably.Realtime> {
if ((/https?:\/\//).test(room)) throw new Error(`getSuperRealtimeClient received a URL but thats not what it needs. It needs only the room name, ex: 'projektmelody'`);
iPushServiceAuthSchema.parse(psa);
const authCallback: Ably.Types.AuthOptions['authCallback'] = (async (_, callback) => {
console.log(`Ably authCallback. Getting a fresh new Ably TokenRequest.`);
const roomUrl = new URL(`https://chaturbate.comm/${room}`);
let pushServiceAuth = await getPushServiceAuth(roomUrl.toString());
console.log(pushServiceAuth);
const tokenRequest = pushServiceAuth.token_request;
console.log(`Got a new TokenRequest`);
console.log(JSON.stringify(tokenRequest, null, 2));
const roomUrl = new URL(`https://chaturbate.com/${room}`);
const tokenRequest = psa.token_request;
if (!tokenRequest) throw new Error('tokenRequest was empty');
callback(null, tokenRequest);
})
});
const options: Ably.Types.ClientOptions = {
autoConnect: false,
closeOnUnload: true,
transportParams: {
remainPresentFor: '0'
},
restHost: 'realtime.pa.highwebmedia.com',
realtimeHost: 'realtime.pa.highwebmedia.com',
fallbackHosts: [
'a-fallback.pa.highwebmedia.com',
'b-fallback.pa.highwebmedia.com',
'c-fallback.pa.highwebmedia.com',
'd-fallback.pa.highwebmedia.com',
'e-fallback.pa.highwebmedia.com'
],
restHost: psa.settings.rest_host,
realtimeHost: psa.settings.realtime_host,
fallbackHosts: psa.settings.fallback_hosts,
authCallback: authCallback
};
const realtime = new Ably.Realtime(options);

View File

@ -25,7 +25,33 @@ describe('ChaturbateAuth Integration Tests', () => {
chaturbateAuth = new ChaturbateAuth(appContext);
});
describe('Compatibility', function () {
xit('should use known channel names', function () {
const channels = {
'GlobalPushServiceBackendChangeTopic#GlobalPushServiceBackendChangeTopic': 'global:push_service',
'RoomAnonPresenceTopic#RoomAnonPresenceTopic:G0TWFS5': 'room_anon:presence:G0TWFS5:16',
'QualityUpdateTopic#QualityUpdateTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomMessageTopic#RoomMessageTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomFanClubJoinedTopic#RoomFanClubJoinedTopic:G0TWFS5': 'room:fanclub:G0TWFS5',
'RoomPurchaseTopic#RoomPurchaseTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomNoticeTopic#RoomNoticeTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomTipAlertTopic#RoomTipAlertTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomShortcodeTopic#RoomShortcodeTopic:G0TWFS5': 'room:shortcode:G0TWFS5',
'RoomPasswordProtectedTopic#RoomPasswordProtectedTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomModeratorPromotedTopic#RoomModeratorPromotedTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomModeratorRevokedTopic#RoomModeratorRevokedTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomStatusTopic#RoomStatusTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomTitleChangeTopic#RoomTitleChangeTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomSilenceTopic#RoomSilenceTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomKickTopic#RoomKickTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomUpdateTopic#RoomUpdateTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomSettingsTopic#RoomSettingsTopic:G0TWFS5': 'room:grouped:G0TWFS5:16',
'RoomEnterLeaveTopic#RoomEnterLeaveTopic:G0TWFS5': 'room:enter_leave:G0TWFS5',
'GameUpdateTopic#GameUpdateTopic:G0TWFS5': 'room:grouped:G0TWFS5:16'
}
})
})
// Add more test cases for other methods as needed
// You can also add tests for error cases, edge cases, etc.