add api server, handle multiple rooms
This commit is contained in:
parent
3500bd68ba
commit
ebec893656
287
index.ts
287
index.ts
|
@ -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)) {
|
||||
|
|
|
@ -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",
|
||||
|
|
383
pnpm-lock.yaml
383
pnpm-lock.yaml
|
@ -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'}
|
||||
|
|
378
src/Room.ts
378
src/Room.ts
|
@ -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 {
|
|||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue