Want to see in action?
* [Demo server](http://peertube.cpy.re)
- * [Video](https://vimeo.com/164881662 "Yes Vimeo, please don't judge me") to see how the "decentralization feature" looks like
+ * [Video](https://peertube.cpy.re/videos/watch/f78a97f8-a142-4ce1-a5bd-154bf9386504) to see how the "decentralization feature" looks like
* Experimental demo servers that share videos (they are in the same network): [peertube2](http://peertube2.cpy.re), [peertube3](http://peertube3.cpy.re). Since I do experiments with them, sometimes they might not work correctly.
## Why
<div class="content-padding">
<h3>Friends list</h3>
- <p-dataTable [value]="friends">
- <p-column field="id" header="ID"></p-column>
- <p-column field="host" header="Host"></p-column>
+ <p-dataTable
+ [value]="friends" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ sortField="id" (onLazyLoad)="loadLazy($event)"
+ >
+ <p-column field="id" header="ID" [sortable]="true"></p-column>
+ <p-column field="host" header="Host" [sortable]="true"></p-column>
<p-column field="email" header="Email"></p-column>
- <p-column field="score" header="Score"></p-column>
- <p-column field="createdAt" header="Created date"></p-column>
+ <p-column field="score" header="Score" [sortable]="true"></p-column>
+ <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Delete" styleClass="action-cell">
<ng-template pTemplate="body" let-pod="rowData">
<span (click)="removeFriend(pod)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this pod"></span>
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
+import { SortMeta } from 'primeng/primeng'
import { ConfirmService } from '../../../core'
-import { FriendService } from '../shared'
+import { RestTable, RestPagination } from '../../../shared'
import { Pod } from '../../../../../../shared'
+import { FriendService } from '../shared'
@Component({
selector: 'my-friend-list',
templateUrl: './friend-list.component.html',
styleUrls: ['./friend-list.component.scss']
})
-export class FriendListComponent implements OnInit {
+export class FriendListComponent extends RestTable implements OnInit {
friends: Pod[] = []
+ totalRecords = 0
+ rowsPerPage = 10
+ sort: SortMeta = { field: 'id', order: 1 }
+ pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
private notificationsService: NotificationsService,
private confirmService: ConfirmService,
private friendService: FriendService
- ) {}
+ ) {
+ super()
+ }
ngOnInit () {
this.loadData()
)
}
- private loadData () {
- this.friendService.getFriends()
+ protected loadData () {
+ this.friendService.getFriends(this.pagination, this.sort)
.subscribe(
resultList => {
this.friends = resultList.data
+ this.totalRecords = resultList.total
},
err => this.notificationsService.error('Error', err.message)
import { Injectable } from '@angular/core'
-import { HttpClient } from '@angular/common/http'
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map'
-import { RestExtractor } from '../../../shared'
+import { SortMeta } from 'primeng/primeng'
+
+import { RestExtractor, RestPagination, RestService } from '../../../shared'
import { Pod, ResultList } from '../../../../../../shared'
@Injectable()
constructor (
private authHttp: HttpClient,
+ private restService: RestService,
private restExtractor: RestExtractor
) {}
- getFriends () {
- return this.authHttp.get<ResultList<Pod>>(FriendService.BASE_FRIEND_URL)
+ getFriends (pagination: RestPagination, sort: SortMeta): Observable<ResultList<Pod>> {
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination, sort)
+
+ return this.authHttp.get<ResultList<Pod>>(FriendService.BASE_FRIEND_URL, { params })
.map(res => this.restExtractor.convertResultListDateToHuman(res))
.catch(res => this.restExtractor.handleError(res))
}
import * as express from 'express'
import { database as db } from '../../initializers/database'
-import { CONFIG } from '../../initializers'
+import { logger, getFormattedObjects } from '../../helpers'
import {
- logger,
- getMyPublicCert,
- getFormattedObjects
-} from '../../helpers'
-import {
- sendOwnedVideosToPod,
makeFriends,
quitFriends,
removeFriend
} from '../../lib'
import {
- podsAddValidator,
authenticate,
ensureIsAdmin,
makeFriendsValidator,
- setBodyHostPort,
setBodyHostsPort,
- podRemoveValidator
+ podRemoveValidator,
+ paginationValidator,
+ setPagination,
+ setPodsSort,
+ podsSortValidator
} from '../../middlewares'
-import {
- PodInstance
-} from '../../models'
-import { Pod as FormattedPod } from '../../../shared'
+import { PodInstance } from '../../models'
const podsRouter = express.Router()
-podsRouter.get('/', listPods)
-podsRouter.post('/',
- setBodyHostPort, // We need to modify the host before running the validator!
- podsAddValidator,
- addPods
+podsRouter.get('/',
+ paginationValidator,
+ podsSortValidator,
+ setPodsSort,
+ setPagination,
+ listPods
)
podsRouter.post('/make-friends',
authenticate,
// ---------------------------------------------------------------------------
-function addPods (req: express.Request, res: express.Response, next: express.NextFunction) {
- const informations = req.body
-
- const pod = db.Pod.build(informations)
- pod.save()
- .then(podCreated => {
- return sendOwnedVideosToPod(podCreated.id)
- })
- .then(() => {
- return getMyPublicCert()
- })
- .then(cert => {
- return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL })
- })
- .catch(err => next(err))
-}
-
function listPods (req: express.Request, res: express.Response, next: express.NextFunction) {
- db.Pod.list()
- .then(podsList => res.json(getFormattedObjects<FormattedPod, PodInstance>(podsList, podsList.length)))
+ db.Pod.listForApi(req.query.start, req.query.count, req.query.sort)
+ .then(resultList => res.json(getFormattedObjects(resultList.data, resultList.total)))
.catch(err => next(err))
}
import * as express from 'express'
import { database as db } from '../../../initializers/database'
-import { checkSignature, signatureValidator } from '../../../middlewares'
-import { PodSignature } from '../../../../shared'
+import {
+ checkSignature,
+ signatureValidator,
+ setBodyHostPort,
+ remotePodsAddValidator
+} from '../../../middlewares'
+import { sendOwnedVideosToPod } from '../../../lib'
+import { getMyPublicCert, getFormattedObjects } from '../../../helpers'
+import { CONFIG } from '../../../initializers'
+import { PodInstance } from '../../../models'
+import { PodSignature, Pod as FormattedPod } from '../../../../shared'
const remotePodsRouter = express.Router()
-// Post because this is a secured request
remotePodsRouter.post('/remove',
signatureValidator,
checkSignature,
removePods
)
+remotePodsRouter.post('/list', remotePodsList)
+
+remotePodsRouter.post('/add',
+ setBodyHostPort, // We need to modify the host before running the validator!
+ remotePodsAddValidator,
+ addPods
+)
+
// ---------------------------------------------------------------------------
export {
// ---------------------------------------------------------------------------
+function addPods (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const information = req.body
+
+ const pod = db.Pod.build(information)
+ pod.save()
+ .then(podCreated => {
+ return sendOwnedVideosToPod(podCreated.id)
+ })
+ .then(() => {
+ return getMyPublicCert()
+ })
+ .then(cert => {
+ return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL })
+ })
+ .catch(err => next(err))
+}
+
+function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) {
+ db.Pod.list()
+ .then(podsList => res.json(getFormattedObjects<FormattedPod, PodInstance>(podsList, podsList.length)))
+ .catch(err => next(err))
+}
+
function removePods (req: express.Request, res: express.Response, next: express.NextFunction) {
const signature: PodSignature = req.body.signature
const host = signature.host
// Sortable columns per schema
const SORTABLE_COLUMNS = {
+ PODS: [ 'id', 'host', 'score', 'createdAt' ],
USERS: [ 'id', 'username', 'createdAt' ],
VIDEO_ABUSES: [ 'id', 'createdAt' ],
VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
function getForeignPodsList (host: string) {
return new Promise< ResultList<FormattedPod> >((res, rej) => {
- const path = '/api/' + API_VERSION + '/pods'
+ const path = '/api/' + API_VERSION + '/remote/pods/list'
- request.get(REMOTE_SCHEME.HTTP + '://' + host + path, (err, response, body) => {
+ request.post(REMOTE_SCHEME.HTTP + '://' + host + path, (err, response, body) => {
if (err) return rej(err)
try {
return Promise.map(podsList, pod => {
const params = {
- url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/pods/',
+ url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/remote/pods/add',
method: 'POST' as 'POST',
json: {
host: CONFIG.WEBSERVER.HOST,
import { SortType } from '../helpers'
import { database } from '../initializers'
+function setPodsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
+ if (!req.query.sort) req.query.sort = '-createdAt'
+
+ return next()
+}
+
function setUsersSort (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.query.sort) req.query.sort = '-createdAt'
// ---------------------------------------------------------------------------
export {
+ setPodsSort,
setUsersSort,
setVideoAbusesSort,
setVideosSort,
import { database as db } from '../../initializers/database'
import { checkErrors } from './utils'
-import { logger, isEachUniqueHostValid, isHostValid } from '../../helpers'
+import { logger, isEachUniqueHostValid } from '../../helpers'
import { CONFIG } from '../../initializers'
import { hasFriends } from '../../lib'
import { isTestInstance } from '../../helpers'
}
]
-const podsAddValidator = [
- body('host').custom(isHostValid).withMessage('Should have a host'),
- body('email').isEmail().withMessage('Should have an email'),
- body('publicKey').not().isEmpty().withMessage('Should have a public key'),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking podsAdd parameters', { parameters: req.body })
-
- checkErrors(req, res, () => {
- db.Pod.loadByHost(req.body.host)
- .then(pod => {
- // Pod with this host already exists
- if (pod) {
- return res.sendStatus(409)
- }
-
- return next()
- })
- .catch(err => {
- logger.error('Cannot load pod by host.', err)
- res.sendStatus(500)
- })
- })
- }
-]
-
const podRemoveValidator = [
param('id').isNumeric().not().isEmpty().withMessage('Should have a valid id'),
export {
makeFriendsValidator,
- podsAddValidator,
podRemoveValidator
}
+export * from './pods'
export * from './signature'
export * from './videos'
--- /dev/null
+import { body } from 'express-validator/check'
+import * as express from 'express'
+
+import { database as db } from '../../../initializers'
+import { isHostValid, logger } from '../../../helpers'
+import { checkErrors } from '../utils'
+
+const remotePodsAddValidator = [
+ body('host').custom(isHostValid).withMessage('Should have a host'),
+ body('email').isEmail().withMessage('Should have an email'),
+ body('publicKey').not().isEmpty().withMessage('Should have a public key'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking podsAdd parameters', { parameters: req.body })
+
+ checkErrors(req, res, () => {
+ db.Pod.loadByHost(req.body.host)
+ .then(pod => {
+ // Pod with this host already exists
+ if (pod) {
+ return res.sendStatus(409)
+ }
+
+ return next()
+ })
+ .catch(err => {
+ logger.error('Cannot load pod by host.', err)
+ res.sendStatus(500)
+ })
+ })
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ remotePodsAddValidator
+}
import { SORTABLE_COLUMNS } from '../../initializers'
// Initialize constants here for better performances
+const SORTABLE_PODS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PODS)
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
+const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS)
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
// ---------------------------------------------------------------------------
export {
+ podsSortValidator,
usersSortValidator,
videoAbusesSortValidator,
videosSortValidator,
import { logger } from '../../helpers'
-function checkErrors (req: express.Request, res: express.Response, next: express.NextFunction, statusCode = 400) {
+function checkErrors (req: express.Request, res: express.Response, next: express.NextFunction) {
const errors = validationResult(req)
if (!errors.isEmpty()) {
logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
- return res.status(statusCode).json({ errors: errors.mapped() })
+ return res.status(400).json({ errors: errors.mapped() })
}
return next()
// Don't use barrel, import just what we need
import { Pod as FormattedPod } from '../../../shared/models/pods/pod.model'
+import { ResultList } from '../../../shared/models/result-list.model'
export namespace PodMethods {
export type ToFormattedJSON = (this: PodInstance) => FormattedPod
export type List = () => Promise<PodInstance[]>
+ export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<PodInstance> >
+
export type ListAllIds = (transaction: Sequelize.Transaction) => Promise<number[]>
export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string) => Promise<number[]>
countAll: PodMethods.CountAll
incrementScores: PodMethods.IncrementScores
list: PodMethods.List
+ listForApi: PodMethods.ListForApi
listAllIds: PodMethods.ListAllIds
listRandomPodIdsWithRequest: PodMethods.ListRandomPodIdsWithRequest
listBadPods: PodMethods.ListBadPods
import { FRIEND_SCORE, PODS_SCORE } from '../../initializers'
import { logger, isHostValid } from '../../helpers'
-import { addMethodsToModel } from '../utils'
+import { addMethodsToModel, getSort } from '../utils'
import {
PodInstance,
PodAttributes,
let countAll: PodMethods.CountAll
let incrementScores: PodMethods.IncrementScores
let list: PodMethods.List
+let listForApi: PodMethods.ListForApi
let listAllIds: PodMethods.ListAllIds
let listRandomPodIdsWithRequest: PodMethods.ListRandomPodIdsWithRequest
let listBadPods: PodMethods.ListBadPods
countAll,
incrementScores,
list,
+ listForApi,
listAllIds,
listRandomPodIdsWithRequest,
listBadPods,
return Pod.findAll()
}
+listForApi = function (start: number, count: number, sort: string) {
+ const query = {
+ offset: start,
+ limit: count,
+ order: [ getSort(sort) ]
+ }
+
+ return Pod.findAndCountAll(query).then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
+}
+
listAllIds = function (transaction: Sequelize.Transaction) {
const query = {
attributes: [ 'id' ],
} from '../../utils'
describe('Test pods API validators', function () {
- const path = '/api/v1/pods/'
let server: ServerInfo
// ---------------------------------------------------------------
})
describe('When managing friends', function () {
+ const path = '/api/v1/pods/'
let userAccessToken = null
before(async function () {
})
})
+ describe('When listing friends', function () {
+ it('Should fail with a bad start pagination', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ start: 'hello' })
+ .set('Accept', 'application/json')
+ .expect(400)
+ })
+
+ it('Should fail with a bad count pagination', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ count: 'hello' })
+ .set('Accept', 'application/json')
+ .expect(400)
+ })
+
+ it('Should fail with an incorrect sort', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ sort: 'hello' })
+ .set('Accept', 'application/json')
+ .expect(400)
+ })
+ })
+
describe('When quitting friends', function () {
it('Should fail with an invalid token', async function () {
await request(server.url)
})
})
- describe('When adding a pod', function () {
+ describe('When adding a pod from remote', function () {
+ const path = '/api/v1/remote/pods/add'
+
it('Should fail with nothing', async function () {
const fields = {}
await makePostBodyRequest({ url: server.url, path, fields })
makeFriends,
getFriendsList,
dateIsValid,
- quitOneFriend
+ quitOneFriend,
+ getPodsListPaginationAndSort
} from '../utils'
describe('Test basic friends', function () {
await makeFriends(server.url, server.accessToken, 409)
})
+ it('Should list friends correctly', async function () {
+ const start = 1
+ const count = 1
+ const sort = '-host'
+
+ const res = await getPodsListPaginationAndSort(servers[0].url, start, count, sort)
+ expect(res.body.total).to.equal(2)
+ expect(res.body.data).to.have.lengthOf(1)
+
+ const pod = res.body.data[0]
+ expect(pod.host).to.equal('localhost:9002')
+ expect(pod.email).to.equal('admin2@example.com')
+ expect(pod.score).to.equal(20)
+ expect(dateIsValid(pod.createdAt)).to.be.true
+ })
+
it('Should quit friends of pod 2', async function () {
this.timeout(10000)
.expect('Content-Type', /json/)
}
+function getPodsListPaginationAndSort (url: string, start: number, count: number, sort: string) {
+ const path = '/api/v1/pods/'
+
+ return request(url)
+ .get(path)
+ .query({ start })
+ .query({ count })
+ .query({ sort })
+ .set('Accept', 'application/json')
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
async function makeFriends (url: string, accessToken: string, expectedStatus = 204) {
// Which pod makes friends with which pod
const friendsMatrix = {
getFriendsList,
makeFriends,
quitFriends,
- quitOneFriend
+ quitOneFriend,
+ getPodsListPaginationAndSort
}