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 {
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()
)
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))
)
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'
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)
)
feedsRouter.get('/feeds/videos.:format',
videosSortValidator,
setDefaultSort,
- asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)),
+ asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.FEEDS)),
asyncMiddleware(videoFeedsValidator),
asyncMiddleware(generateVideoFeed)
)
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())
)
// 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)
+)
// ---------------------------------------------------------------------------
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()
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<any>, savedFields: object) {
Object.keys(savedFields).forEach(key => {
const value = savedFields[key]
}
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
}
}
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'
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 => {
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)
}
}
+const cache = (duration: number | string) => {
+ const _lifetime = parseDuration(duration, 3600000)
+ return cacheRoute(_lifetime)
+}
+
// ---------------------------------------------------------------------------
export {
- cacheRoute
+ cacheRoute,
+ cache
}
--- /dev/null
+/**
+ * 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
+ }
+}