"sequelize": "4.41.2",
"sequelize-typescript": "0.6.6",
"sharp": "^0.21.0",
+ "sitemap": "^2.1.0",
"srt-to-vtt": "^1.1.2",
"summon-install": "^0.4.3",
"useragent": "^2.3.0",
dropRedis () {
redis-cli KEYS "bull-localhost:900$1*" | grep -v empty | xargs --no-run-if-empty redis-cli DEL
+ redis-cli KEYS "redis-localhost:900$1*" | grep -v empty | xargs --no-run-if-empty redis-cli DEL
}
for i in $(seq 1 6); do
servicesRouter,
webfingerRouter,
trackerRouter,
- createWebsocketServer
+ createWebsocketServer, botsRouter
} from './server/controllers'
import { advertiseDoNotTrack } from './server/middlewares/dnt'
import { Redis } from './server/lib/redis'
app.use('/', feedsRouter)
app.use('/', webfingerRouter)
app.use('/', trackerRouter)
+app.use('/', botsRouter)
// Static files
app.use('/', staticRouter)
--- /dev/null
+import * as express from 'express'
+import { asyncMiddleware } from '../middlewares'
+import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers'
+import * as sitemapModule from 'sitemap'
+import { logger } from '../helpers/logger'
+import { VideoModel } from '../models/video/video'
+import { VideoChannelModel } from '../models/video/video-channel'
+import { AccountModel } from '../models/account/account'
+import { cacheRoute } from '../middlewares/cache'
+import { buildNSFWFilter } from '../helpers/express-utils'
+import { truncate } from 'lodash'
+
+const botsRouter = express.Router()
+
+// Special route that add OpenGraph and oEmbed tags
+// Do not use a template engine for a so little thing
+botsRouter.use('/sitemap.xml',
+ asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)),
+ asyncMiddleware(getSitemap)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ botsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function getSitemap (req: express.Request, res: express.Response) {
+ let urls = getSitemapBasicUrls()
+
+ urls = urls.concat(await getSitemapLocalVideoUrls())
+ urls = urls.concat(await getSitemapVideoChannelUrls())
+ urls = urls.concat(await getSitemapAccountUrls())
+
+ const sitemap = sitemapModule.createSitemap({
+ hostname: CONFIG.WEBSERVER.URL,
+ urls: urls
+ })
+
+ sitemap.toXML((err, xml) => {
+ if (err) {
+ logger.error('Cannot generate sitemap.', { err })
+ return res.sendStatus(500)
+ }
+
+ res.header('Content-Type', 'application/xml')
+ res.send(xml)
+ })
+}
+
+async function getSitemapVideoChannelUrls () {
+ const rows = await VideoChannelModel.listLocalsForSitemap('createdAt')
+
+ return rows.map(channel => ({
+ url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername
+ }))
+}
+
+async function getSitemapAccountUrls () {
+ const rows = await AccountModel.listLocalsForSitemap('createdAt')
+
+ return rows.map(channel => ({
+ url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername
+ }))
+}
+
+async function getSitemapLocalVideoUrls () {
+ const resultList = await VideoModel.listForApi({
+ start: 0,
+ count: undefined,
+ sort: 'createdAt',
+ includeLocalVideos: true,
+ nsfw: buildNSFWFilter(),
+ filter: 'local',
+ withFiles: false
+ })
+
+ return resultList.data.map(v => ({
+ url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid,
+ video: [
+ {
+ title: v.name,
+ // Sitemap description should be < 2000 characters
+ description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
+ player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid,
+ thumbnail_loc: v.getThumbnailStaticPath()
+ }
+ ]
+ }))
+}
+
+function getSitemapBasicUrls () {
+ const paths = [
+ '/about/instance',
+ '/videos/local'
+ ]
+
+ return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p }))
+}
export * from './static'
export * from './webfinger'
export * from './tracker'
+export * from './bots'
import { isArray } from './custom-validators/misc'
import { UserModel } from '../models/account/user'
-function buildNSFWFilter (res: express.Response, paramNSFW?: string) {
+function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
if (paramNSFW === 'true') return true
if (paramNSFW === 'false') return false
if (paramNSFW === 'both') return undefined
- if (res.locals.oauth) {
+ if (res && res.locals.oauth) {
const user: UserModel = res.locals.oauth.token.User
// User does not want NSFW videos
const ROUTE_CACHE_LIFETIME = {
FEEDS: '15 minutes',
ROBOTS: '2 hours',
+ SITEMAP: '1 day',
SECURITYTXT: '2 hours',
NODEINFO: '10 minutes',
DNT_POLICY: '1 week',
})
}
+ static listLocalsForSitemap (sort: string) {
+ const query = {
+ attributes: [ ],
+ offset: 0,
+ order: getSort(sort),
+ include: [
+ {
+ attributes: [ 'preferredUsername', 'serverId' ],
+ model: ActorModel.unscoped(),
+ where: {
+ serverId: null
+ }
+ }
+ ]
+ }
+
+ return AccountModel
+ .unscoped()
+ .findAll(query)
+ }
+
toFormattedJSON (): Account {
const actor = this.Actor.toFormattedJSON()
const account = {
})
}
+ static listLocalsForSitemap (sort: string) {
+ const query = {
+ attributes: [ ],
+ offset: 0,
+ order: getSort(sort),
+ include: [
+ {
+ attributes: [ 'preferredUsername', 'serverId' ],
+ model: ActorModel.unscoped(),
+ where: {
+ serverId: null
+ }
+ }
+ ]
+ }
+
+ return VideoChannelModel
+ .unscoped()
+ .findAll(query)
+ }
+
static searchForApi (options: {
actorId: number
search: string
import 'mocha'
import * as chai from 'chai'
-import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from './utils'
+import {
+ addVideoChannel,
+ createUser,
+ flushTests,
+ killallServers,
+ makeGetRequest,
+ runServer,
+ ServerInfo,
+ setAccessTokensToServers,
+ uploadVideo
+} from './utils'
+import { VideoPrivacy } from '../../shared/models/videos'
const expect = chai.expect
await flushTests()
server = await runServer(1)
+ await setAccessTokensToServers([ server ])
})
describe('Test a well known endpoints', function () {
})
})
+ describe('Test bots endpoints', function () {
+
+ it('Should get the empty sitemap', async function () {
+ const res = await makeGetRequest({
+ url: server.url,
+ path: '/sitemap.xml',
+ statusCodeExpected: 200
+ })
+
+ expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
+ })
+
+ it('Should get the empty cached sitemap', async function () {
+ const res = await makeGetRequest({
+ url: server.url,
+ path: '/sitemap.xml',
+ statusCodeExpected: 200
+ })
+
+ expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
+ })
+
+ it('Should add videos, channel and accounts and get sitemap', async function () {
+ this.timeout(35000)
+
+ await uploadVideo(server.url, server.accessToken, { name: 'video 1', nsfw: false })
+ await uploadVideo(server.url, server.accessToken, { name: 'video 2', nsfw: false })
+ await uploadVideo(server.url, server.accessToken, { name: 'video 3', privacy: VideoPrivacy.PRIVATE })
+
+ await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' })
+ await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' })
+
+ await createUser(server.url, server.accessToken, 'user1', 'password')
+ await createUser(server.url, server.accessToken, 'user2', 'password')
+
+ const res = await makeGetRequest({
+ url: server.url,
+ path: '/sitemap.xml?t=1', // avoid using cache
+ statusCodeExpected: 200
+ })
+
+ expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
+
+ expect(res.text).to.contain('<video:title><![CDATA[video 1]]></video:title>')
+ expect(res.text).to.contain('<video:title><![CDATA[video 2]]></video:title>')
+ expect(res.text).to.not.contain('<video:title><![CDATA[video 3]]></video:title>')
+
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel1</loc></url>')
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel2</loc></url>')
+
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user1</loc></url>')
+ expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user2</loc></url>')
+ })
+ })
+
after(async function () {
killallServers([ server ])
})
readable-stream "^2.0.5"
ws "^6.0.0"
+sitemap@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-2.1.0.tgz#1633cb88c196d755ad94becfb1c1bcacc6d3425a"
+ integrity sha512-AkfA7RDVCITQo+j5CpXsMJlZ/8ENO2NtgMHYIh+YMvex2Hao/oe3MQgNa03p0aWY6srCfUA1Q02OgiWCAiuccA==
+ dependencies:
+ lodash "^4.17.10"
+ url-join "^4.0.0"
+ xmlbuilder "^10.0.0"
+
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+url-join@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
+ integrity sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=
+
url-parse-lax@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=
+xmlbuilder@^10.0.0:
+ version "10.1.1"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0"
+ integrity sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==
+
xmlbuilder@~9.0.1:
version "9.0.7"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"