diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 3e6361906..ebb2c06a2 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -16,7 +16,7 @@ import { VideoModel } from '../../models/video/video' import { VideoChannelModel } from '../../models/video/video-channel' import { VideoCommentModel } from '../../models/video/video-comment' import { VideoShareModel } from '../../models/video/video-share' -import { cacheRoute } from '../../middlewares/cache' +import { cache } from '../../middlewares/cache' import { activityPubResponse } from './utils' import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { @@ -25,7 +25,6 @@ import { getVideoLikesActivityPubUrl, getVideoSharesActivityPubUrl } from '../../lib/activitypub' -import { VideoCaption } from '../../../shared/models/videos/video-caption.model' import { VideoCaptionModel } from '../../models/video/video-caption' const activityPubClientRouter = express.Router() @@ -44,7 +43,7 @@ activityPubClientRouter.get('/accounts?/:name/following', ) activityPubClientRouter.get('/videos/watch/:id', - executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), + executeIfActivityPub(asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), executeIfActivityPub(asyncMiddleware(videosGetValidator)), executeIfActivityPub(asyncMiddleware(videoController)) ) diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 682f4abda..6cbe42a2a 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -5,7 +5,7 @@ import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeed import { VideoModel } from '../models/video/video' import * as Feed from 'pfeed' import { AccountModel } from '../models/account/account' -import { cacheRoute } from '../middlewares/cache' +import { cache } from '../middlewares/cache' import { VideoChannelModel } from '../models/video/video-channel' import { VideoCommentModel } from '../models/video/video-comment' import { buildNSFWFilter } from '../helpers/express-utils' @@ -13,7 +13,7 @@ import { buildNSFWFilter } from '../helpers/express-utils' const feedsRouter = express.Router() feedsRouter.get('/feeds/video-comments.:format', - asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), + asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.FEEDS)), asyncMiddleware(videoCommentsFeedsValidator), asyncMiddleware(generateVideoCommentsFeed) ) @@ -21,7 +21,7 @@ feedsRouter.get('/feeds/video-comments.:format', feedsRouter.get('/feeds/videos.:format', videosSortValidator, setDefaultSort, - asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), + asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.FEEDS)), asyncMiddleware(videoFeedsValidator), asyncMiddleware(generateVideoFeed) ) diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 8de9c5a78..ce5d0c5fa 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts @@ -1,11 +1,16 @@ import * as cors from 'cors' import * as express from 'express' -import { CONFIG, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' +import { CONFIG, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS, ROUTE_CACHE_LIFETIME } from '../initializers' import { VideosPreviewCache } from '../lib/cache' +import { cache } from '../middlewares/cache' import { asyncMiddleware, videosGetValidator } from '../middlewares' import { VideoModel } from '../models/video/video' import { VideosCaptionCache } from '../lib/cache/videos-caption-cache' +import { UserModel } from '../models/account/user' +import { VideoCommentModel } from '../models/video/video-comment' +import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../models/nodeinfo' +const packageJSON = require('../../../package.json') const staticRouter = express.Router() staticRouter.use(cors()) @@ -65,10 +70,32 @@ staticRouter.use( ) // robots.txt service -staticRouter.get('/robots.txt', (req: express.Request, res: express.Response) => { - res.type('text/plain') - return res.send(CONFIG.INSTANCE.ROBOTS) -}) +staticRouter.get('/robots.txt', + asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.ROBOTS)), + (_, res: express.Response) => { + res.type('text/plain') + return res.send(CONFIG.INSTANCE.ROBOTS) + } +) + +// nodeinfo service +staticRouter.use('/.well-known/nodeinfo', + asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.NODEINFO)), + (_, res: express.Response) => { + return res.json({ + links: [ + { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: CONFIG.WEBSERVER.URL + '/nodeinfo/2.0.json' + } + ] + }) + } +) +staticRouter.use('/nodeinfo/:version.json', + asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.NODEINFO)), + asyncMiddleware(generateNodeinfo) +) // --------------------------------------------------------------------------- @@ -95,6 +122,54 @@ async function getVideoCaption (req: express.Request, res: express.Response) { return res.sendFile(path, { maxAge: STATIC_MAX_AGE }) } +async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) { + const { totalVideos } = await VideoModel.getStats() + const { totalLocalVideoComments } = await VideoCommentModel.getStats() + const { totalUsers } = await UserModel.getStats() + let json = {} + + if (req.params.version && (req.params.version === '2.0')) { + json = { + version: '2.0', + software: { + name: 'peertube', + version: packageJSON.version + }, + protocols: [ + 'activitypub' + ], + services: { + inbound: [], + outbound: [ + 'atom1.0', + 'rss2.0' + ] + }, + openRegistrations: CONFIG.SIGNUP.ENABLED, + usage: { + users: { + total: totalUsers + }, + localPosts: totalVideos, + localComments: totalLocalVideoComments + }, + metadata: { + taxonomy: { + postsName: 'Videos' + }, + nodeName: CONFIG.INSTANCE.NAME, + nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION + } + } as HttpNodeinfoDiasporaSoftwareNsSchema20 + res.set('Content-Type', 'application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8') + } else { + json = { error: 'Nodeinfo schema version not handled' } + res.status(404) + } + + return res.end(JSON.stringify(json)) +} + async function downloadTorrent (req: express.Request, res: express.Response, next: express.NextFunction) { const { video, videoFile } = getVideoAndFile(req, res) if (!videoFile) return res.status(404).end() diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 8fa861281..834d788c8 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -104,6 +104,36 @@ function computeResolutionsToTranscode (videoFileHeight: number) { return resolutionsEnabled } +const timeTable = { + ms: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 3600000 * 24, + week: 3600000 * 24 * 7, + month: 3600000 * 24 * 30 +} +export function parseDuration (duration: number | string, defaultDuration: number): number { + if (typeof duration === 'number') return duration + + if (typeof duration === 'string') { + const split = duration.match(/^([\d\.,]+)\s?(\w+)$/) + + if (split.length === 3) { + const len = parseFloat(split[1]) + let unit = split[2].replace(/s$/i,'').toLowerCase() + if (unit === 'm') { + unit = 'ms' + } + + return (len || 1) * (timeTable[unit] || 0) + } + } + + logger.error('Duration could not be properly parsed, defaulting to ' + defaultDuration) + return defaultDuration +} + function resetSequelizeInstance (instance: Model, savedFields: object) { Object.keys(savedFields).forEach(key => { const value = savedFields[key] diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e66ebb662..e8e0da683 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -46,9 +46,11 @@ const OAUTH_LIFETIME = { } const ROUTE_CACHE_LIFETIME = { - FEEDS: 1000 * 60 * 15, // 15 minutes + FEEDS: '15 minutes', + ROBOTS: '2 hours', + NODEINFO: '10 minutes', ACTIVITY_PUB: { - VIDEOS: 1000 // 1 second, cache concurrent requests after a broadcast for example + VIDEOS: '1 second' // 1 second, cache concurrent requests after a broadcast for example } } diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts index 1de44db70..1e5a13b2e 100644 --- a/server/middlewares/cache.ts +++ b/server/middlewares/cache.ts @@ -1,5 +1,6 @@ import * as express from 'express' import * as AsyncLock from 'async-lock' +import { parseDuration } from '../helpers/utils' import { Redis } from '../lib/redis' import { logger } from '../helpers/logger' @@ -20,7 +21,7 @@ function cacheRoute (lifetime: number) { res.send = (body) => { if (res.statusCode >= 200 && res.statusCode < 400) { - const contentType = res.getHeader('content-type').toString() + const contentType = res.get('content-type') Redis.Instance.setCachedRoute(req, body, lifetime, contentType, res.statusCode) .then(() => done()) .catch(err => { @@ -35,7 +36,7 @@ function cacheRoute (lifetime: number) { return next() } - if (cached.contentType) res.contentType(cached.contentType) + if (cached.contentType) res.set('content-type', cached.contentType) if (cached.statusCode) { const statusCode = parseInt(cached.statusCode, 10) @@ -50,8 +51,14 @@ function cacheRoute (lifetime: number) { } } +const cache = (duration: number | string) => { + const _lifetime = parseDuration(duration, 3600000) + return cacheRoute(_lifetime) +} + // --------------------------------------------------------------------------- export { - cacheRoute + cacheRoute, + cache } diff --git a/server/models/nodeinfo/index.d.ts b/server/models/nodeinfo/index.d.ts new file mode 100644 index 000000000..0a2d0492e --- /dev/null +++ b/server/models/nodeinfo/index.d.ts @@ -0,0 +1,117 @@ +/** + * NodeInfo schema version 2.0. + */ +export interface HttpNodeinfoDiasporaSoftwareNsSchema20 { + /** + * The schema version, must be 2.0. + */ + version: '2.0' + /** + * Metadata about server software in use. + */ + software: { + /** + * The canonical name of this server software. + */ + name: string + /** + * The version of this server software. + */ + version: string + } + /** + * The protocols supported on this server. + */ + protocols: ( + | 'activitypub' + | 'buddycloud' + | 'dfrn' + | 'diaspora' + | 'libertree' + | 'ostatus' + | 'pumpio' + | 'tent' + | 'xmpp' + | 'zot')[] + /** + * The third party sites this server can connect to via their application API. + */ + services: { + /** + * The third party sites this server can retrieve messages from for combined display with regular traffic. + */ + inbound: ('atom1.0' | 'gnusocial' | 'imap' | 'pnut' | 'pop3' | 'pumpio' | 'rss2.0' | 'twitter')[] + /** + * The third party sites this server can publish messages to on the behalf of a user. + */ + outbound: ( + | 'atom1.0' + | 'blogger' + | 'buddycloud' + | 'diaspora' + | 'dreamwidth' + | 'drupal' + | 'facebook' + | 'friendica' + | 'gnusocial' + | 'google' + | 'insanejournal' + | 'libertree' + | 'linkedin' + | 'livejournal' + | 'mediagoblin' + | 'myspace' + | 'pinterest' + | 'pnut' + | 'posterous' + | 'pumpio' + | 'redmatrix' + | 'rss2.0' + | 'smtp' + | 'tent' + | 'tumblr' + | 'twitter' + | 'wordpress' + | 'xmpp')[] + } + /** + * Whether this server allows open self-registration. + */ + openRegistrations: boolean + /** + * Usage statistics for this server. + */ + usage: { + /** + * statistics about the users of this server. + */ + users: { + /** + * The total amount of on this server registered users. + */ + total?: number + /** + * The amount of users that signed in at least once in the last 180 days. + */ + activeHalfyear?: number + /** + * The amount of users that signed in at least once in the last 30 days. + */ + activeMonth?: number + }; + /** + * The amount of posts that were made by users that are registered on this server. + */ + localPosts?: number + /** + * The amount of comments that were made by users that are registered on this server. + */ + localComments?: number + } + /** + * Free form key value pairs for software specific values. Clients should not rely on any specific key present. + */ + metadata: { + [k: string]: any + } +}