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.
216 lines
6.5 KiB
216 lines
6.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,
|
|
},
|
|
{
|
|
path: 'lastStatus',
|
|
},
|
|
];
|
|
}
|
|
|
|
channelMiddleware ( ) {
|
|
return async (req, res, next) => {
|
|
try {
|
|
res.locals.venue = res.locals.venue || { };
|
|
res.locals.venue.channels = await VenueChannel
|
|
.find()
|
|
.populate(this.populateVenueChannel)
|
|
.lean();
|
|
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);
|
|
|
|
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(),
|
|
};
|
|
|
|
await channel.save();
|
|
await this.updateChannelStatus(channel);
|
|
|
|
return channel.toObject();
|
|
}
|
|
|
|
async updateChannel (channel, channelDefinition) {
|
|
const updateOp = { $set: { } };
|
|
|
|
updateOp.$set.slug = this.getChannelSlug(channelDefinition.url);
|
|
|
|
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({ withCredentials: false }, options);
|
|
pagination = Object.assign({ skip: 0, cpp: 10 }, pagination);
|
|
|
|
const search = { };
|
|
|
|
let q = VenueChannel
|
|
.find(search)
|
|
.sort({ slug: 1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp);
|
|
|
|
if (options.withCredentials) {
|
|
q = q.select('+credentials');
|
|
}
|
|
return q.populate(this.populateVenueChannel).lean();
|
|
}
|
|
|
|
async getChannelById (channelId, options) {
|
|
options = Object.assign({ withCredentials: false }, options);
|
|
let q = VenueChannel.findOne({ _id: channelId });
|
|
if (options.withCredentials) {
|
|
q = q.select('+credentials');
|
|
}
|
|
return q.populate(this.populateVenueChannel).lean();
|
|
}
|
|
|
|
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');
|
|
}
|
|
return q.populate(this.populateVenueChannel).lean();
|
|
}
|
|
|
|
async getChannelFeed (channelSlug, options) {
|
|
const { cache: cacheService } = this.dtp.services;
|
|
const cacheKey = `venue:ch:${channelSlug}`;
|
|
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/${channelSlug}/feed/json`;
|
|
this.log.info('fetching Shing channel feed', { channelSlug, 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}`);
|
|
}
|
|
|
|
let status = new VenueChannelStatus(json.channel);
|
|
status.created = new Date();
|
|
status.channel = channel._id;
|
|
|
|
await status.save();
|
|
await VenueChannel.updateOne({ _id: channel._id }, { $set: { lastStatus: status._id } });
|
|
|
|
return status.toObject();
|
|
}
|
|
|
|
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()));
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
slug: 'venue',
|
|
name: 'venue',
|
|
create: (dtp) => { return new VenueService(dtp); },
|
|
}; |