adding initial support for nodeinfo

This commit is contained in:
Rigel Kent 2018-07-21 23:00:25 +02:00 committed by Chocobozzz
parent 4278710d5b
commit 3f6d68d967
7 changed files with 246 additions and 16 deletions

View File

@ -16,7 +16,7 @@ import { VideoModel } from '../../models/video/video'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import { VideoCommentModel } from '../../models/video/video-comment' import { VideoCommentModel } from '../../models/video/video-comment'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { cacheRoute } from '../../middlewares/cache' import { cache } from '../../middlewares/cache'
import { activityPubResponse } from './utils' import { activityPubResponse } from './utils'
import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { import {
@ -25,7 +25,6 @@ import {
getVideoLikesActivityPubUrl, getVideoLikesActivityPubUrl,
getVideoSharesActivityPubUrl getVideoSharesActivityPubUrl
} from '../../lib/activitypub' } from '../../lib/activitypub'
import { VideoCaption } from '../../../shared/models/videos/video-caption.model'
import { VideoCaptionModel } from '../../models/video/video-caption' import { VideoCaptionModel } from '../../models/video/video-caption'
const activityPubClientRouter = express.Router() const activityPubClientRouter = express.Router()
@ -44,7 +43,7 @@ activityPubClientRouter.get('/accounts?/:name/following',
) )
activityPubClientRouter.get('/videos/watch/:id', 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(videosGetValidator)),
executeIfActivityPub(asyncMiddleware(videoController)) executeIfActivityPub(asyncMiddleware(videoController))
) )

View File

@ -5,7 +5,7 @@ import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeed
import { VideoModel } from '../models/video/video' import { VideoModel } from '../models/video/video'
import * as Feed from 'pfeed' import * as Feed from 'pfeed'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { cacheRoute } from '../middlewares/cache' import { cache } from '../middlewares/cache'
import { VideoChannelModel } from '../models/video/video-channel' import { VideoChannelModel } from '../models/video/video-channel'
import { VideoCommentModel } from '../models/video/video-comment' import { VideoCommentModel } from '../models/video/video-comment'
import { buildNSFWFilter } from '../helpers/express-utils' import { buildNSFWFilter } from '../helpers/express-utils'
@ -13,7 +13,7 @@ import { buildNSFWFilter } from '../helpers/express-utils'
const feedsRouter = express.Router() const feedsRouter = express.Router()
feedsRouter.get('/feeds/video-comments.:format', feedsRouter.get('/feeds/video-comments.:format',
asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.FEEDS)),
asyncMiddleware(videoCommentsFeedsValidator), asyncMiddleware(videoCommentsFeedsValidator),
asyncMiddleware(generateVideoCommentsFeed) asyncMiddleware(generateVideoCommentsFeed)
) )
@ -21,7 +21,7 @@ feedsRouter.get('/feeds/video-comments.:format',
feedsRouter.get('/feeds/videos.:format', feedsRouter.get('/feeds/videos.:format',
videosSortValidator, videosSortValidator,
setDefaultSort, setDefaultSort,
asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.FEEDS)),
asyncMiddleware(videoFeedsValidator), asyncMiddleware(videoFeedsValidator),
asyncMiddleware(generateVideoFeed) asyncMiddleware(generateVideoFeed)
) )

View File

@ -1,11 +1,16 @@
import * as cors from 'cors' import * as cors from 'cors'
import * as express from 'express' 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 { VideosPreviewCache } from '../lib/cache'
import { cache } from '../middlewares/cache'
import { asyncMiddleware, videosGetValidator } from '../middlewares' import { asyncMiddleware, videosGetValidator } from '../middlewares'
import { VideoModel } from '../models/video/video' import { VideoModel } from '../models/video/video'
import { VideosCaptionCache } from '../lib/cache/videos-caption-cache' 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() const staticRouter = express.Router()
staticRouter.use(cors()) staticRouter.use(cors())
@ -65,10 +70,32 @@ staticRouter.use(
) )
// robots.txt service // robots.txt service
staticRouter.get('/robots.txt', (req: express.Request, res: express.Response) => { staticRouter.get('/robots.txt',
res.type('text/plain') asyncMiddleware(cache(ROUTE_CACHE_LIFETIME.ROBOTS)),
return res.send(CONFIG.INSTANCE.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 }) 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) { async function downloadTorrent (req: express.Request, res: express.Response, next: express.NextFunction) {
const { video, videoFile } = getVideoAndFile(req, res) const { video, videoFile } = getVideoAndFile(req, res)
if (!videoFile) return res.status(404).end() if (!videoFile) return res.status(404).end()

View File

@ -104,6 +104,36 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
return resolutionsEnabled 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) { function resetSequelizeInstance (instance: Model<any>, savedFields: object) {
Object.keys(savedFields).forEach(key => { Object.keys(savedFields).forEach(key => {
const value = savedFields[key] const value = savedFields[key]

View File

@ -46,9 +46,11 @@ const OAUTH_LIFETIME = {
} }
const ROUTE_CACHE_LIFETIME = { const ROUTE_CACHE_LIFETIME = {
FEEDS: 1000 * 60 * 15, // 15 minutes FEEDS: '15 minutes',
ROBOTS: '2 hours',
NODEINFO: '10 minutes',
ACTIVITY_PUB: { 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
} }
} }

View File

@ -1,5 +1,6 @@
import * as express from 'express' import * as express from 'express'
import * as AsyncLock from 'async-lock' import * as AsyncLock from 'async-lock'
import { parseDuration } from '../helpers/utils'
import { Redis } from '../lib/redis' import { Redis } from '../lib/redis'
import { logger } from '../helpers/logger' import { logger } from '../helpers/logger'
@ -20,7 +21,7 @@ function cacheRoute (lifetime: number) {
res.send = (body) => { res.send = (body) => {
if (res.statusCode >= 200 && res.statusCode < 400) { 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) Redis.Instance.setCachedRoute(req, body, lifetime, contentType, res.statusCode)
.then(() => done()) .then(() => done())
.catch(err => { .catch(err => {
@ -35,7 +36,7 @@ function cacheRoute (lifetime: number) {
return next() return next()
} }
if (cached.contentType) res.contentType(cached.contentType) if (cached.contentType) res.set('content-type', cached.contentType)
if (cached.statusCode) { if (cached.statusCode) {
const statusCode = parseInt(cached.statusCode, 10) 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 { export {
cacheRoute cacheRoute,
cache
} }

117
server/models/nodeinfo/index.d.ts vendored Normal file
View File

@ -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
}
}