adding initial support for nodeinfo
authorRigel Kent <sendmemail@rigelk.eu>
Sat, 21 Jul 2018 21:00:25 +0000 (23:00 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 24 Jul 2018 12:08:44 +0000 (14:08 +0200)
server/controllers/activitypub/client.ts
server/controllers/feeds.ts
server/controllers/static.ts
server/helpers/utils.ts
server/initializers/constants.ts
server/middlewares/cache.ts
server/models/nodeinfo/index.d.ts [new file with mode: 0644]

index 3e636190676471a67715eebddc624d59f0d5df59..ebb2c06a22bc7f58c1d3396835d215ab376b08a1 100644 (file)
@@ -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))
 )
index 682f4abdaf873d56087f4b8b242b8afa0066e487..6cbe42a2ad842edede1d54249a4dbdfcd5a5297b 100644 (file)
@@ -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)
 )
index 8de9c5a78e4359446071e9289e171df39809cb4e..ce5d0c5fa2b2bdad0b795e33cd7b1c5fa4773f67 100644 (file)
@@ -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()
index 8fa861281610b4b0aa67900ed29fd1a510d8eced..834d788c85a168337275d540656aeb8e3aa896ee 100644 (file)
@@ -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<any>, savedFields: object) {
   Object.keys(savedFields).forEach(key => {
     const value = savedFields[key]
index e66ebb6626dddd4a4336c97840f2503a4199252f..e8e0da683f6dff072db3d5304dc7e4a104f08d4d 100644 (file)
@@ -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
   }
 }
 
index 1de44db703429a171386489a5914d474441141b4..1e5a13b2e0394952409d71e6db8acb23cdf3a5ac 100644 (file)
@@ -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 (file)
index 0000000..0a2d049
--- /dev/null
@@ -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
+  }
+}