You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

240 lines
7.5 KiB

// venue.js
// Copyright (C) 2022 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const VenueChannel = mongoose.model('VenueChannel');
const VenueChannelStatus = mongoose.model('VenueChannelStatus');
const https = require('https');
const fetch = require('node-fetch'); // jshint ignore:line
const striptags = require('striptags');
const slug = require('slug');
const CACHE_DURATION = 60 * 5;
const { SiteService, SiteError } = require('../../lib/site-lib');
class VenueService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
this.soapboxDomain = process.env.DTP_SOAPBOX_HOST || 'shing.tv';
}
async start ( ) {
const { user: userService } = this.dtp.services;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
this.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
this.populateVenueChannel = [
{
path: 'owner',
select: userService.USER_SELECT,
},
];
}
channelMiddleware ( ) {
return async (req, res, next) => {
try {
res.locals.venue = res.locals.venue || { };
res.locals.venue.channels = [ ];
await VenueChannel
.find()
.populate(this.populateVenueChannel)
.lean()
.cursor()
.eachAsync(async (channel) => {
channel.currentStatus = await this.updateChannelStatus(channel);
res.locals.venue.channels.push(channel);
});
return next();
} catch (error) {
this.log.error('failed to populate Soapbox channel feed', { error });
return next();
}
};
}
async createChannel (owner, channelDefinition) {
const channel = new VenueChannel();
channel.ownerType = owner.type;
channel.owner = owner._id;
channel.slug = this.getChannelSlug(channelDefinition.url);
channel.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10);
if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
throw new SiteError(400, 'Must provide a stream key');
}
if (!channelDefinition['credentials.widgetKey'] || (channelDefinition['credentials.widgetKey'] === '')) {
throw new SiteError(400, 'Must provide a widget key');
}
channel.credentials = {
streamKey: channelDefinition['credentials.streamKey'].trim(),
widgetKey: channelDefinition['credentials.widgetKey'].trim(),
};
const status = await this.updateChannelStatus(channel);
channel.name = status.name;
channel.description = status.description;
await channel.save();
await this.updateChannelStatus(channel);
return channel.toObject();
}
async updateChannel (channel, owner, channelDefinition) {
const updateOp = { $set: { } };
updateOp.$set.ownerType = owner.type;
updateOp.$set.owner = owner._id;
updateOp.$set.slug = this.getChannelSlug(channelDefinition.url);
updateOp.$set.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10);
const status = await this.updateChannelStatus(channel);
updateOp.$set.name = status.name;
updateOp.$set.description = status.description;
if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
throw new SiteError(400, 'Must provide a stream key');
}
updateOp.$set['credentials.streamKey'] = channelDefinition['credentials.streamKey'].trim();
if (!channelDefinition['credentials.widgetKey'] || (channelDefinition['credentials.widgetKey'] === '')) {
throw new SiteError(400, 'Must provide a widget key');
}
updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim();
channel = await VenueChannel.findOneAndUpdate({ _id: channel._id }, updateOp, { new: true });
await this.updateChannelStatus(channel);
}
async getChannels (pagination, options) {
options = Object.assign({ featuredOnly: false, withCredentials: false }, options);
const search = { };
let q = VenueChannel
.find(search)
.sort({ sortOrder: 1, name: 1, slug: 1 });
if (pagination) {
q = q.skip(pagination.skip).limit(pagination.cpp);
}
if (options.withCredentials) {
q = q.select('+credentials');
}
const channels = await q.populate(this.populateVenueChannel).lean();
for await (const channel of channels) {
channel.currentStatus = await this.updateChannelStatus(channel);
}
return channels;
}
async getChannelById (channelId, options) {
options = Object.assign({ withCredentials: false }, options);
let q = VenueChannel.findOne({ _id: channelId });
if (options.withCredentials) {
q = q.select('+credentials');
}
const channel = await q.populate(this.populateVenueChannel).lean();
channel.currentStatus = await this.updateChannelStatus(channel);
return channel;
}
async getChannelBySlug (channelSlug, options) {
options = Object.assign({ withCredentials: false }, options);
let q = VenueChannel.findOne({ slug: channelSlug.toLowerCase().trim() });
if (options.withCredentials) {
q = q.select('+credentials');
}
const channel = await q.populate(this.populateVenueChannel).lean();
channel.currentStatus = await this.updateChannelStatus(channel);
return channel;
}
async getChannelFeed (channel, options) {
const { cache: cacheService } = this.dtp.services;
const cacheKey = `venue:ch:${channel.slug}`;
options = Object.assign({ allowCache: true }, options);
let json;
if (options.allowCache) {
json = await cacheService.getObject(cacheKey);
if (json) { return json; }
}
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/feed/json`;
this.log.info('fetching Shing channel feed', { slug: channel.slug, requestUrl });
const response = await fetch(requestUrl, {
agent: this.httpsAgent,
});
if (!response.ok) {
throw new SiteError(500, `Failed to fetch Shing channel feed: ${response.statusText}`);
}
json = await response.json();
await cacheService.setObjectEx(cacheKey, CACHE_DURATION, json);
return json;
}
async updateChannelStatus (channel) {
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`;
this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl });
const response = await fetch(requestUrl, { agent: this.httpsAgent });
if (!response.ok) {
throw new SiteError(500, `Failed to fetch channel status: ${response.statusText}`);
}
const json = await response.json();
if (!json.success) {
throw new Error(`failed to fetch channel status: ${json.message}`);
}
return json.channel;
}
getChannelSlug (channelUrl) {
const { URL } = require('url');
const url = new URL(channelUrl);
if (url.host !== this.soapboxDomain) {
throw new SiteError(400, 'This is not a valid DTP stream channel URL: Domain mismatch.');
}
const channelUrlParts = url.pathname.split('/').filter((part) => part.length > 0);
if (channelUrlParts[0] !== 'channel') {
throw new SiteError(400, 'This is not a valid DTP stream channel URL: Not on channel path.');
}
return slug(striptags(channelUrlParts[1].trim()));
}
async removeChannel (channel) {
await VenueChannelStatus.deleteMany({ channel: channel._id });
await VenueChannel.deleteOne({ _id: channel._id });
}
}
module.exports = {
slug: 'venue',
name: 'venue',
create: (dtp) => { return new VenueService(dtp); },
};