Merge from upstream

This commit is contained in:
buoyantair 2018-11-16 02:37:16 +05:30
commit ae28cdf327
118 changed files with 4970 additions and 17172 deletions

View File

@ -2,13 +2,15 @@
Interested in contributing? Awesome! Interested in contributing? Awesome!
**Quick Links:** **This guide will present you the following contribution topics:**
* [Translate](#translate) * [Translate](#translate)
* [Give your feedback](#give-your-feedback) * [Give your feedback](#give-your-feedback)
* [Write documentation](#write-documentation) * [Write documentation](#write-documentation)
* [Develop](#develop) * [Develop](#develop)
* [Improve the website](#improve-the-website)
* [Troubleshooting](#troubleshooting)
* [Tutorials](#tutorials)
## Translate ## Translate
@ -37,6 +39,15 @@ Some hints:
* Models sent/received by the controllers are defined in [/shared/models](/shared/models) directory * Models sent/received by the controllers are defined in [/shared/models](/shared/models) directory
## Improve the website
PeerTube's website is [joinpeertube.org](https://joinpeertube.org), where people can learn about the project and how it works note that it is not a PeerTube instance, but rather the project's homepage.
You can help us improve it too!
It is not hosted on GitHub but on [Framasoft](https://framasoft.org/)'s own [GitLab](https://about.gitlab.com/) instance, [FramaGit](https://framagit.org): https://framagit.org/framasoft/peertube/joinpeertube
## Develop ## Develop
Don't hesitate to talk about features you want to develop by creating/commenting an issue Don't hesitate to talk about features you want to develop by creating/commenting an issue
@ -122,37 +133,7 @@ and the web server is automatically restarted.
$ npm run dev $ npm run dev
``` ```
Depending on your OS, you may face the following error : ### Testing the federation of PeerTube servers
```
$ [nodemon] Internal watch failed: ENOSPC: no space left on device, watch '/PeerTube/dist'
```
This is due to your system's limit on the number of files you can monitor for live-checking changes. For example, Ubuntu uses inotify and this limit is set to 8192. Then you need to change this limit :
```
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
```
See more information here : https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers
### Configurations for VPS
If you want to develop using a Virtual Private Server, you will need to configure the url for the API and the hostname. First, you need to edit the [client/src/environments/environment.hmr.ts](client/src/environments/environment.hmr.ts) file by replacing the `localhost` in the `apiUrl` field with the address of your VPS. Thus, the [Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/) from Webpack will be set up for developping with live-reload.
Next, you will need to edit the [config/default.yaml](config/default.yaml) file. Just replace the `localhost` with your VPS address in the following `hostname` fields :
```
listen:
hostname: 'my-vps-address.net'
port: 9000
webserver:
https: false
hostname: 'my-vps-address.net'
port: 9000
```
Then, you just need to listen to `https://my-vps-address.net:3000/` in your web browser.
### Federation
Create a PostgreSQL user **with the same name as your username** in order to avoid using the *postgres* user. Create a PostgreSQL user **with the same name as your username** in order to avoid using the *postgres* user.
Then, we can create the databases (if they don't already exist): Then, we can create the databases (if they don't already exist):
@ -206,3 +187,11 @@ $ npm run mocha -- --exit --require ts-node/register/type-check --bail server/te
Instance configurations are in `config/test-{1,2,3,4,5,6}.yaml`. Instance configurations are in `config/test-{1,2,3,4,5,6}.yaml`.
Note that only instance 2 has transcoding enabled. Note that only instance 2 has transcoding enabled.
### Troubleshooting
Please check out the issues and [list of common errors](https://docs.joinpeertube.org/lang/en/devdocs/troubleshooting.html).
### Tutorials
Please check out the related section in the [development documentation](https://docs.joinpeertube.org/lang/en/devdocs/index.html#tutorials). Contribute tutorials at [framagit.org/framasoft/peertube/documentation](https://framagit.org/framasoft/peertube/documentation).

View File

@ -1,13 +1,13 @@
<h1 align="center"> <h1 align="center">
<a href="https://joinpeertube.org"> <a href="https://joinpeertube.org">
<img src="https://joinpeertube.org/img/brand.png" alt="PeerTube"> <img src="https://joinpeertube.org/img/brand.png" alt="PeerTube">
<a> </a>
</h1> </h1>
<p align=center> <p align=center>
<strong><a href="https://joinpeertube.org">Website</a></strong> <strong><a href="https://joinpeertube.org">Website</a></strong>
| <strong><a href="https://instances.joinpeertube.org">Join an instance</a></strong> | <strong><a href="https://instances.joinpeertube.org">Join an instance</a></strong>
| <strong><a href="#package-create-your-own-instance">Create one</a></strong> | <strong><a href="#package-create-your-own-instance">Create an instance</a></strong>
| <strong><a href="#contact">Chat with us</a></strong> | <strong><a href="#contact">Chat with us</a></strong>
</p> </p>
@ -75,7 +75,7 @@ Just upload your videos, and be sure they will stream anywhere. Add a descriptio
<h3 align="right">Keep in touch with video creators</h3> <h3 align="right">Keep in touch with video creators</h3>
<p align="right"> <p align="right">
Follow your favorite channels from PeerTube or really any other place. No need to have an account on the instance you watched a video to follow its author, you can do all of that from the Fediverse (Mastodon, Pleroma and plenty others), or just with good ol' RSS. Follow your favorite channels from PeerTube or really any other place. No need to have an account on the instance you watched a video to follow its author, you can do all of that from the Fediverse (Mastodon, Pleroma, and plenty others), or just with good ol' RSS.
</p> </p>
--- ---
@ -121,6 +121,24 @@ enough because one video could become popular and overload the server. That is
why we need to use a P2P protocol to limit the server load. Thanks to why we need to use a P2P protocol to limit the server load. Thanks to
[WebTorrent](https://github.com/feross/webtorrent), we can make BitTorrent inside the web browser, as of today. [WebTorrent](https://github.com/feross/webtorrent), we can make BitTorrent inside the web browser, as of today.
:raised_hands: Contributing
----------------------------------------------------------------
You don't need to be a coder to help!
You can give us your feedback, report bugs, help us translate PeerTube, write documentation, and more. Check out the [contributing
guide](/.github/CONTRIBUTING.md) to know how, it takes less than 2 minutes to get started. :wink:
You can also join the cheerful bunch that makes our community:
* Chat<a name="contact"></a>:
* IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
* Matrix (bridged on the IRC channel) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)**
* Forum:
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
Feel free to reach out if you have any questions or ideas! :speech_balloon:
:package: Create your own instance :package: Create your own instance
---------------------------------------------------------------- ----------------------------------------------------------------
@ -137,20 +155,6 @@ See the [production guide](/support/doc/production.md), which is the recommended
See the [community packages](https://docs.joinpeertube.org/lang/en/docs/install.html), which cover various platforms (including [YunoHost](https://install-app.yunohost.org/?app=peertube) and [Docker](/support/doc/docker.md)). See the [community packages](https://docs.joinpeertube.org/lang/en/docs/install.html), which cover various platforms (including [YunoHost](https://install-app.yunohost.org/?app=peertube) and [Docker](/support/doc/docker.md)).
:wrench: Contribute/Translate/Test
----------------------------------------------------------------
*Spoiler alert*: you don't need to be a coder to help!
See the [contributing
guide](/.github/CONTRIBUTING.md). Or simply join the cheerful bunch that makes our community:
* Chat<a name="contact"></a>:
* IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
* Matrix (bridged on the IRC channel) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)**
* Forum:
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
:book: Documentation :book: Documentation
---------------------------------------------------------------- ----------------------------------------------------------------
@ -179,9 +183,8 @@ See [ARCHITECTURE.md](/ARCHITECTURE.md) for a more detailed explanation of the a
#### Backend #### Backend
* REST API: * REST API:
* Quick Start: [/support/doc/api/quickstart.md](/support/doc/api/quickstart.md) * OpenAPI 3.0.0 schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml)
* Swagger/OpenAPI schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml) * HTML explorer: [docs.joinpeertube.org/api.html](http://docs.joinpeertube.org/api.html)
* HTML explorer: [/support/doc/api/html/index.html](https://htmlpreview.github.io/?https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/api/html/index.html)
* Servers communicate with each other with [Activity * Servers communicate with each other with [Activity
Pub](https://www.w3.org/TR/activitypub/). Pub](https://www.w3.org/TR/activitypub/).
* Each server has its own users who query it (search videos, query where the * Each server has its own users who query it (search videos, query where the

View File

@ -63,23 +63,23 @@
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts" "setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.8.3", "@angular-devkit/build-angular": "~0.10.0",
"@angular/animations": "~6.1.4", "@angular/animations": "~7.0.2",
"@angular/cli": "~6.2.3", "@angular/cli": "~7.0.4",
"@angular/common": "~6.1.4", "@angular/common": "~7.0.2",
"@angular/compiler": "~6.1.4", "@angular/compiler": "~7.0.2",
"@angular/compiler-cli": "~6.1.4", "@angular/compiler-cli": "~7.0.2",
"@angular/core": "~6.1.4", "@angular/core": "~7.0.2",
"@angular/forms": "~6.1.4", "@angular/forms": "~7.0.2",
"@angular/http": "~6.1.4", "@angular/http": "~7.0.2",
"@angular/language-service": "~6.1.4", "@angular/language-service": "~7.0.2",
"@angular/platform-browser": "~6.1.4", "@angular/platform-browser": "~7.0.2",
"@angular/platform-browser-dynamic": "~6.1.4", "@angular/platform-browser-dynamic": "~7.0.2",
"@angular/router": "~6.1.4", "@angular/router": "~7.0.2",
"@angular/service-worker": "~6.1.4", "@angular/service-worker": "~7.0.2",
"@angularclass/hmr": "^2.1.3", "@angularclass/hmr": "^2.1.3",
"@neos21/bootstrap3-glyphicons": "^1.0.1", "@neos21/bootstrap3-glyphicons": "^1.0.1",
"@ng-bootstrap/ng-bootstrap": "^3.1.0", "@ng-bootstrap/ng-bootstrap": "^4.0.0",
"@ngx-loading-bar/core": "^2.2.0", "@ngx-loading-bar/core": "^2.2.0",
"@ngx-loading-bar/http-client": "^2.2.0", "@ngx-loading-bar/http-client": "^2.2.0",
"@ngx-loading-bar/router": "^2.2.0", "@ngx-loading-bar/router": "^2.2.0",
@ -129,7 +129,6 @@
"ngx-clipboard": "11.1.7", "ngx-clipboard": "11.1.7",
"ngx-pipes": "^2.1.7", "ngx-pipes": "^2.1.7",
"ngx-qrcode2": "^0.0.9", "ngx-qrcode2": "^0.0.9",
"ngx-textarea-autosize": "^2.0.0",
"node-sass": "^4.9.3", "node-sass": "^4.9.3",
"npm-font-source-sans-pro": "^1.0.2", "npm-font-source-sans-pro": "^1.0.2",
"path-browserify": "^1.0.0", "path-browserify": "^1.0.0",
@ -139,17 +138,17 @@
"purify-css": "^1.2.5", "purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0", "purifycss-webpack": "^0.7.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"rxjs": "^6.1.0", "rxjs": "^6.3.3",
"sanitize-html": "^1.18.4", "sanitize-html": "^1.18.4",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"sass-resources-loader": "^1.2.1", "sass-resources-loader": "^2.0.0",
"stream-browserify": "^2.0.1", "stream-browserify": "^2.0.1",
"stream-http": "^2.8.3", "stream-http": "^3.0.0",
"terser-webpack-plugin": "^1.1.0", "terser-webpack-plugin": "^1.1.0",
"ts-jest": "^23.1.4", "ts-jest": "^23.1.4",
"tslint": "^5.7.0", "tslint": "^5.7.0",
"tslint-config-standard": "^8.0.1", "tslint-config-standard": "^8.0.1",
"typescript": "2.9", "typescript": "3.1.6",
"video.js": "^7", "video.js": "^7",
"videojs-contextmenu-ui": "^5.0.0", "videojs-contextmenu-ui": "^5.0.0",
"videojs-dock": "^2.0.2", "videojs-dock": "^2.0.2",

View File

@ -4,10 +4,10 @@ import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
import { ConfigService } from '@app/+admin/config/shared/config.service' import { ConfigService } from '@app/+admin/config/shared/config.service'
export abstract class UserEdit extends FormReactive { export abstract class UserEdit extends FormReactive {
videoQuotaOptions: { value: string, label: string }[] = [] videoQuotaOptions: { value: string, label: string }[] = []
videoQuotaDailyOptions: { value: string, label: string }[] = [] videoQuotaDailyOptions: { value: string, label: string }[] = []
roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
username: string
protected abstract serverService: ServerService protected abstract serverService: ServerService
protected abstract configService: ConfigService protected abstract configService: ConfigService

View File

@ -86,4 +86,4 @@
</ng-template> </ng-template>
</p-table> </p-table>
<my-user-ban-modal #userBanModal (userBanned)="onUsersBanned()"></my-user-ban-modal> <my-user-ban-modal #userBanModal (userBanned)="onUserChanged()"></my-user-ban-modal>

View File

@ -66,7 +66,7 @@ export class UserListComponent extends RestTable implements OnInit {
this.userBanModal.openModal(users) this.userBanModal.openModal(users)
} }
onUsersBanned () { onUserChanged () {
this.loadData() this.loadData()
} }

View File

@ -4,7 +4,11 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
export abstract class MyAccountVideoChannelEdit extends FormReactive { export abstract class MyAccountVideoChannelEdit extends FormReactive {
// We need it even in the create component because it's used in the edit template // We need it even in the create component because it's used in the edit template
videoChannelToUpdate: VideoChannel videoChannelToUpdate: VideoChannel
instanceHost: string
abstract isCreation (): boolean abstract isCreation (): boolean
abstract getFormButtonTitle (): string abstract getFormButtonTitle (): string
// FIXME: We need this method so angular does not complain in the child template
onAvatarChange (formData: FormData) { /* empty */ }
} }

View File

@ -25,7 +25,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string titlePage: string
marginContent = false // Disable margin marginContent = false // Disable margin
currentRoute = '/video-channel/videos' currentRoute = '/video-channels/videos'
loadOnInit = false loadOnInit = false
private videoChannel: VideoChannel private videoChannel: VideoChannel
@ -55,7 +55,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
this.videoChannelSub = this.videoChannelService.videoChannelLoaded this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.subscribe(videoChannel => { .subscribe(videoChannel => {
this.videoChannel = videoChannel this.videoChannel = videoChannel
this.currentRoute = '/video-channel/' + this.videoChannel.uuid + '/videos' this.currentRoute = '/video-channels/' + this.videoChannel.uuid + '/videos'
this.reloadVideos() this.reloadVideos()
this.generateSyndicationList() this.generateSyndicationList()

View File

@ -50,7 +50,7 @@
.icon.icon-upload { .icon.icon-upload {
@include icon(22px); @include icon(22px);
background-image: url('../../assets/images/header/upload.svg'); background-image: url('../../assets/images/header/upload-white.svg');
height: 24px; height: 24px;
vertical-align: middle; vertical-align: middle;
margin-right: 6px; margin-right: 6px;

View File

@ -131,10 +131,14 @@ menu {
transition: background-color .1s ease-in-out; transition: background-color .1s ease-in-out;
@include disable-default-a-behaviour; @include disable-default-a-behaviour;
&:hover, &.focus-visible { &.active {
background-color: rgba(255, 255, 255, 0.15); background-color: rgba(255, 255, 255, 0.15);
} }
&:hover, &.focus-visible {
background-color: rgba(255, 255, 255, 0.10);
}
.icon { .icon {
@include icon(22px); @include icon(22px);

View File

@ -1,3 +1,4 @@
export * from './form-validators' export * from './form-validators'
export * from './form-reactive' export * from './form-reactive'
export * from './reactive-file.component' export * from './reactive-file.component'
export * from './textarea-autoresize.directive'

View File

@ -0,0 +1,25 @@
// Thanks: https://github.com/evseevdev/ngx-textarea-autosize
import { AfterViewInit, Directive, ElementRef, HostBinding, HostListener } from '@angular/core'
@Directive({
selector: 'textarea[myAutoResize]'
})
export class TextareaAutoResizeDirective implements AfterViewInit {
@HostBinding('attr.rows') rows = '1'
@HostBinding('style.overflow') overflow = 'hidden'
constructor (private elem: ElementRef) { }
public ngAfterViewInit () {
this.resize()
}
@HostListener('input')
resize () {
const textarea = this.elem.nativeElement as HTMLTextAreaElement
// Reset textarea height to auto that correctly calculate the new height
textarea.style.height = 'auto'
// Set new height
textarea.style.height = `${textarea.scrollHeight}px`
}
}

View File

@ -7,8 +7,9 @@ export class FromNowPipe implements PipeTransform {
constructor (private i18n: I18n) { } constructor (private i18n: I18n) { }
transform (value: number) { transform (arg: number | Date | string) {
const seconds = Math.floor((Date.now() - value) / 1000) const argDate = new Date(arg)
const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
let interval = Math.floor(seconds / 31536000) let interval = Math.floor(seconds / 31536000)
if (interval > 1) { if (interval > 1) {

View File

@ -37,13 +37,15 @@ import {
LoginValidatorsService, LoginValidatorsService,
ReactiveFileComponent, ReactiveFileComponent,
ResetPasswordValidatorsService, ResetPasswordValidatorsService,
TextareaAutoResizeDirective,
UserValidatorsService, UserValidatorsService,
VideoAbuseValidatorsService, VideoAbuseValidatorsService,
VideoAcceptOwnershipValidatorsService,
VideoBlacklistValidatorsService, VideoBlacklistValidatorsService,
VideoChangeOwnershipValidatorsService,
VideoChannelValidatorsService, VideoChannelValidatorsService,
VideoCommentValidatorsService, VideoCommentValidatorsService,
VideoValidatorsService, VideoValidatorsService
VideoChangeOwnershipValidatorsService, VideoAcceptOwnershipValidatorsService
} from '@app/shared/forms' } from '@app/shared/forms'
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
import { ScreenService } from '@app/shared/misc/screen.service' import { ScreenService } from '@app/shared/misc/screen.service'
@ -53,7 +55,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c
import { VideoImportService } from '@app/shared/video-import/video-import.service' import { VideoImportService } from '@app/shared/video-import/video-import.service'
import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription' import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
import { OverviewService } from '@app/shared/overview' import { OverviewService } from '@app/shared/overview'
import { UserBanModalComponent } from '@app/shared/moderation' import { UserBanModalComponent } from '@app/shared/moderation'
@ -92,6 +94,7 @@ import { BlocklistService } from '@app/shared/blocklist'
FromNowPipe, FromNowPipe,
MarkdownTextareaComponent, MarkdownTextareaComponent,
InfiniteScrollerDirective, InfiniteScrollerDirective,
TextareaAutoResizeDirective,
HelpComponent, HelpComponent,
ReactiveFileComponent, ReactiveFileComponent,
PeertubeCheckboxComponent, PeertubeCheckboxComponent,
@ -129,6 +132,7 @@ import { BlocklistService } from '@app/shared/blocklist'
ActionDropdownComponent, ActionDropdownComponent,
MarkdownTextareaComponent, MarkdownTextareaComponent,
InfiniteScrollerDirective, InfiniteScrollerDirective,
TextareaAutoResizeDirective,
HelpComponent, HelpComponent,
ReactiveFileComponent, ReactiveFileComponent,
PeertubeCheckboxComponent, PeertubeCheckboxComponent,

View File

@ -7,10 +7,10 @@
class="video-miniature-name" class="video-miniature-name"
[routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
> >
{{ video.name }}
<span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span> <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
<span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
{{ video.name }}
</a> </a>
<span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>

View File

@ -6,11 +6,11 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr
import { ResultList } from '../../../../../shared/models/result-list.model' import { ResultList } from '../../../../../shared/models/result-list.model'
import { import {
UserVideoRate, UserVideoRate,
UserVideoRateType,
UserVideoRateUpdate, UserVideoRateUpdate,
VideoConstant, VideoConstant,
VideoFilter, VideoFilter,
VideoPrivacy, VideoPrivacy,
VideoRateType,
VideoUpdate VideoUpdate
} from '../../../../../shared/models/videos' } from '../../../../../shared/models/videos'
import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
@ -332,7 +332,7 @@ export class VideoService implements VideosProvider {
return privacies return privacies
} }
private setVideoRate (id: number, rateType: VideoRateType) { private setVideoRate (id: number, rateType: UserVideoRateType) {
const url = VideoService.BASE_VIDEO_URL + id + '/rate' const url = VideoService.BASE_VIDEO_URL + id + '/rate'
const body: UserVideoRateUpdate = { const body: UserVideoRateUpdate = {
rating: rateType rating: rateType

View File

@ -3,7 +3,7 @@
<img [src]="getAvatarUrl()" alt="Avatar" /> <img [src]="getAvatarUrl()" alt="Avatar" />
<div class="form-group"> <div class="form-group">
<textarea i18n-placeholder placeholder="Add comment..." autosize <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
[readonly]="(user === null) ? true : false" [readonly]="(user === null) ? true : false"
(click)="openVisitorModal($event)" (click)="openVisitorModal($event)"
formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"

View File

@ -29,9 +29,9 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
@Output() commentCreated = new EventEmitter<VideoCommentCreate>() @Output() commentCreated = new EventEmitter<VideoCommentCreate>()
@ViewChild('visitorModal') visitorModal: NgbModal @ViewChild('visitorModal') visitorModal: NgbModal
@ViewChild('textarea') private textareaElement: ElementRef @ViewChild('textarea') textareaElement: ElementRef
private addingComment = false addingComment = false
constructor ( constructor (
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,

View File

@ -450,7 +450,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.checkUserRating() this.checkUserRating()
} }
private setRating (nextRating: VideoRateType) { private setRating (nextRating: UserVideoRateType) {
let method let method
switch (nextRating) { switch (nextRating) {
case 'like': case 'like':
@ -476,7 +476,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
) )
} }
private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
let likesToIncrement = 0 let likesToIncrement = 0
let dislikesToIncrement = 0 let dislikesToIncrement = 0

View File

@ -17,7 +17,6 @@ import { NgxQRCodeModule } from 'ngx-qrcode2'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component' import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
@NgModule({ @NgModule({
imports: [ imports: [
@ -26,7 +25,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
ClipboardModule, ClipboardModule,
NgbTooltipModule, NgbTooltipModule,
NgxQRCodeModule, NgxQRCodeModule,
TextareaAutosizeModule,
RecommendationsModule RecommendationsModule
], ],

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -47,12 +47,6 @@ import 'core-js/es7/object'
// For Google Bot // For Google Bot
import 'core-js/es6/reflect' import 'core-js/es6/reflect'
/**
* Evergreen browsers require these.
*/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect'
/** /**
* Required to support Web Animations `@angular/platform-browser/animations`. * Required to support Web Animations `@angular/platform-browser/animations`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation

View File

@ -29,7 +29,7 @@
display: block; display: block;
/* Fallback for non-webkit */ /* Fallback for non-webkit */
display: -webkit-box; display: -webkit-box;
max-height: $font-size*$line-height*$lines-to-show + 0.2; max-height: $font-size * $line-height * $lines-to-show;
/* Fallback for non-webkit */ /* Fallback for non-webkit */
font-size: $font-size; font-size: $font-size;
line-height: $line-height; line-height: $line-height;
@ -511,4 +511,4 @@
} }
} }
} }
} }

View File

@ -256,9 +256,8 @@ class PeerTubeEmbed {
} }
private async initCore () { private async initCore () {
const urlParts = window.location.href.split('/') const urlParts = window.location.pathname.split('/')
const lastPart = urlParts[ urlParts.length - 1 ] const videoId = urlParts[ urlParts.length - 1 ]
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[ 0 ]
const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
loadLocaleInVideoJS(window.location.origin, vjs, navigator.language), loadLocaleInVideoJS(window.location.origin, vjs, navigator.language),

File diff suppressed because it is too large Load Diff

View File

@ -43,19 +43,18 @@
"dev:server": "scripty", "dev:server": "scripty",
"dev:client": "scripty", "dev:client": "scripty",
"start": "node dist/server", "start": "node dist/server",
"start:server": "node dist/server --no-client",
"update-host": "node ./dist/scripts/update-host.js", "update-host": "node ./dist/scripts/update-host.js",
"create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js", "create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js",
"create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js", "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js",
"test": "scripty", "test": "scripty",
"help": "scripty", "help": "scripty",
"generate-api-doc": "scripty",
"generate-cli-doc": "scripty", "generate-cli-doc": "scripty",
"parse-log": "node ./dist/scripts/parse-log.js", "parse-log": "node ./dist/scripts/parse-log.js",
"prune-storage": "node ./dist/scripts/prune-storage.js", "prune-storage": "node ./dist/scripts/prune-storage.js",
"optimize-old-videos": "node ./dist/scripts/optimize-old-videos.js", "optimize-old-videos": "node ./dist/scripts/optimize-old-videos.js",
"postinstall": "cd client && yarn install --pure-lockfile", "postinstall": "cd client && yarn install --pure-lockfile",
"tsc": "tsc", "tsc": "tsc",
"spectacle-docs": "node_modules/spectacle-docs/bin/spectacle.js",
"commander": "commander", "commander": "commander",
"ng": "ng", "ng": "ng",
"nodemon": "nodemon", "nodemon": "nodemon",
@ -71,13 +70,21 @@
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "lint-staged" "pre-commit": "./scripts/openapi-peertube-version.sh && lint-staged"
} }
}, },
"lint-staged": { "lint-staged": {
"*.scss": [ "*.scss": [
"sass-lint -c client/.sass-lint.yml", "sass-lint -c client/.sass-lint.yml",
"git add" "git add"
],
"support/doc/api/*.yaml": [
"node ./node_modules/swagger-cli/bin/swagger-cli.js validate support/doc/api/openapi.yaml",
"git add"
],
"server/tools/README.md": [
"npm run generate-cli-doc",
"git add"
] ]
}, },
"resolutions": { "resolutions": {
@ -88,7 +95,7 @@
"async": "^2.0.0", "async": "^2.0.0",
"async-lock": "^1.1.2", "async-lock": "^1.1.2",
"async-lru": "^1.1.1", "async-lru": "^1.1.1",
"bcrypt": "2", "bcrypt": "3.0.2",
"bittorrent-tracker": "^9.0.0", "bittorrent-tracker": "^9.0.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"body-parser": "^1.12.4", "body-parser": "^1.12.4",
@ -113,7 +120,7 @@
"http-signature": "^1.2.0", "http-signature": "^1.2.0",
"ip-anonymize": "^0.0.6", "ip-anonymize": "^0.0.6",
"ipaddr.js": "1.8.1", "ipaddr.js": "1.8.1",
"is-cidr": "^2.0.5", "is-cidr": "^3.0.0",
"iso-639-3": "^1.0.1", "iso-639-3": "^1.0.1",
"js-yaml": "^3.5.4", "js-yaml": "^3.5.4",
"jsonld": "^1.0.1", "jsonld": "^1.0.1",
@ -138,9 +145,9 @@
"request": "^2.81.0", "request": "^2.81.0",
"safe-buffer": "^5.0.1", "safe-buffer": "^5.0.1",
"scripty": "^1.5.0", "scripty": "^1.5.0",
"sequelize": "4.38.0", "sequelize": "4.41.2",
"sequelize-typescript": "0.6.6", "sequelize-typescript": "0.6.6",
"sharp": "^0.20.0", "sharp": "^0.21.0",
"srt-to-vtt": "^1.1.2", "srt-to-vtt": "^1.1.2",
"summon-install": "^0.4.3", "summon-install": "^0.4.3",
"useragent": "^2.3.0", "useragent": "^2.3.0",
@ -155,7 +162,7 @@
"devDependencies": { "devDependencies": {
"@types/async": "^2.0.40", "@types/async": "^2.0.40",
"@types/async-lock": "^1.1.0", "@types/async-lock": "^1.1.0",
"@types/bcrypt": "^2.0.0", "@types/bcrypt": "^3.0.0",
"@types/bluebird": "3.5.21", "@types/bluebird": "3.5.21",
"@types/body-parser": "^1.16.3", "@types/body-parser": "^1.16.3",
"@types/bull": "^3.3.12", "@types/bull": "^3.3.12",
@ -183,7 +190,7 @@
"@types/pem": "^1.9.3", "@types/pem": "^1.9.3",
"@types/redis": "^2.8.5", "@types/redis": "^2.8.5",
"@types/request": "^2.0.3", "@types/request": "^2.0.3",
"@types/sharp": "^0.17.6", "@types/sharp": "^0.21.0",
"@types/supertest": "^2.0.3", "@types/supertest": "^2.0.3",
"@types/validator": "^9.4.0", "@types/validator": "^9.4.0",
"@types/webtorrent": "^0.98.4", "@types/webtorrent": "^0.98.4",
@ -192,19 +199,19 @@
"chai-json-schema": "^1.5.0", "chai-json-schema": "^1.5.0",
"chai-xml": "^0.3.2", "chai-xml": "^0.3.2",
"husky": "^1.0.0-rc.4", "husky": "^1.0.0-rc.4",
"libxmljs": "0.19.3", "libxmljs": "0.19.5",
"lint-staged": "^7.1.0", "lint-staged": "^8.0.4",
"maildev": "^1.0.0-rc3", "maildev": "^1.0.0-rc3",
"mocha": "^5.0.0", "mocha": "^5.0.0",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"sass-lint": "^1.12.1", "sass-lint": "^1.12.1",
"source-map-support": "^0.5.0", "source-map-support": "^0.5.0",
"spectacle-docs": "^1.0.2",
"supertest": "^3.0.0", "supertest": "^3.0.0",
"swagger-cli": "^2.2.0",
"ts-node": "7.0.1", "ts-node": "7.0.1",
"tslint": "^5.7.0", "tslint": "^5.7.0",
"tslint-config-standard": "^8.0.1", "tslint-config-standard": "^8.0.1",
"typescript": "^2.5.2", "typescript": "^3.1.6",
"xliff": "^4.0.0" "xliff": "^4.0.0"
}, },
"scripty": { "scripty": {

View File

@ -1,5 +0,0 @@
#!/bin/sh
set -eu
npm run spectacle-docs -- -t support/doc/api/html support/doc/api/openapi.yaml

View File

@ -0,0 +1,4 @@
# Version key/value should be on his own line
PACKAGE_VERSION=$(node -p "require('./package.json').version")
sed -i "s/\(^\s*\)version: .*/\1version: $PACKAGE_VERSION/" support/doc/api/openapi.yaml

View File

@ -12,7 +12,6 @@ killall -q peertube || true
if [ "$1" = "misc" ]; then if [ "$1" = "misc" ]; then
npm run build -- --light-fr npm run build -- --light-fr
mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/client.ts \ mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/client.ts \
server/tests/activitypub.ts \
server/tests/feeds/index.ts \ server/tests/feeds/index.ts \
server/tests/misc-endpoints.ts \ server/tests/misc-endpoints.ts \
server/tests/helpers/index.ts server/tests/helpers/index.ts
@ -31,7 +30,7 @@ elif [ "$1" = "api-2" ]; then
elif [ "$1" = "api-3" ]; then elif [ "$1" = "api-3" ]; then
npm run build:server npm run build:server
mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-3.ts mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-3.ts
elif [ "$1" = "api-3" ]; then elif [ "$1" = "api-4" ]; then
npm run build:server npm run build:server
mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-4.ts mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-4.ts
elif [ "$1" = "lint" ]; then elif [ "$1" = "lint" ]; then

View File

@ -4,7 +4,7 @@ import { VideoModel } from '../server/models/video/video'
import { ActorModel } from '../server/models/activitypub/actor' import { ActorModel } from '../server/models/activitypub/actor'
import { import {
getAccountActivityPubUrl, getAccountActivityPubUrl,
getAnnounceActivityPubUrl, getVideoAnnounceActivityPubUrl,
getVideoActivityPubUrl, getVideoChannelActivityPubUrl, getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
getVideoCommentActivityPubUrl getVideoCommentActivityPubUrl
} from '../server/lib/activitypub' } from '../server/lib/activitypub'
@ -78,7 +78,7 @@ async function run () {
console.log('Updating video share ' + videoShare.url) console.log('Updating video share ' + videoShare.url)
videoShare.url = getAnnounceActivityPubUrl(videoShare.Video.url, videoShare.Actor) videoShare.url = getVideoAnnounceActivityPubUrl(videoShare.Actor, videoShare.Video)
await videoShare.save() await videoShare.save()
} }

View File

@ -16,6 +16,7 @@ import * as cookieParser from 'cookie-parser'
import * as helmet from 'helmet' import * as helmet from 'helmet'
import * as useragent from 'useragent' import * as useragent from 'useragent'
import * as anonymize from 'ip-anonymize' import * as anonymize from 'ip-anonymize'
import * as cli from 'commander'
process.title = 'peertube' process.title = 'peertube'
@ -27,7 +28,7 @@ import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-be
// Do not use barrels because we don't want to load all modules here (we need to initialize database first) // Do not use barrels because we don't want to load all modules here (we need to initialize database first)
import { logger } from './server/helpers/logger' import { logger } from './server/helpers/logger'
import { API_VERSION, CONFIG, CACHE } from './server/initializers/constants' import { API_VERSION, CONFIG, CACHE, HTTP_SIGNATURE } from './server/initializers/constants'
const missed = checkMissedConfig() const missed = checkMissedConfig()
if (missed.length !== 0) { if (missed.length !== 0) {
@ -95,9 +96,14 @@ import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-
import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler' import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler' import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
// ----------- Command line ----------- // ----------- Command line -----------
cli
.option('--no-client', 'Start PeerTube without client interface')
.parse(process.argv)
// ----------- App ----------- // ----------- App -----------
// Enable CORS for develop // Enable CORS for develop
@ -126,7 +132,11 @@ app.use(morgan('combined', {
app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json({ app.use(bodyParser.json({
type: [ 'application/json', 'application/*+json' ], type: [ 'application/json', 'application/*+json' ],
limit: '500kb' limit: '500kb',
verify: (req: express.Request, _, buf: Buffer, encoding: string) => {
const valid = isHTTPSignatureDigestValid(buf, req)
if (valid !== true) throw new Error('Invalid digest')
}
})) }))
// Cookies // Cookies
app.use(cookieParser()) app.use(cookieParser())
@ -151,7 +161,7 @@ app.use('/', trackerRouter)
app.use('/', staticRouter) app.use('/', staticRouter)
// Client files, last valid routes! // Client files, last valid routes!
app.use('/', clientsRouter) if (cli.client) app.use('/', clientsRouter)
// ----------- Errors ----------- // ----------- Errors -----------

View File

@ -3,17 +3,22 @@ import * as express from 'express'
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers'
import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send' import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send'
import { audiencify, getAudience } from '../../lib/activitypub/audience' import { audiencify, getAudience } from '../../lib/activitypub/audience'
import { buildCreateActivity } from '../../lib/activitypub/send/send-create' import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
import { import {
asyncMiddleware, asyncMiddleware,
videosShareValidator,
executeIfActivityPub, executeIfActivityPub,
localAccountValidator, localAccountValidator,
localVideoChannelValidator, localVideoChannelValidator,
videosCustomGetValidator videosCustomGetValidator
} from '../../middlewares' } from '../../middlewares'
import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' import {
getAccountVideoRateValidator,
videoCommentGetValidator,
videosGetValidator
} from '../../middlewares/validators'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor' import { ActorModel } from '../../models/activitypub/actor'
import { ActorFollowModel } from '../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../models/activitypub/actor-follow'
@ -25,6 +30,7 @@ import { cacheRoute } 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 {
getRateUrl,
getVideoCommentsActivityPubUrl, getVideoCommentsActivityPubUrl,
getVideoDislikesActivityPubUrl, getVideoDislikesActivityPubUrl,
getVideoLikesActivityPubUrl, getVideoLikesActivityPubUrl,
@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
executeIfActivityPub(asyncMiddleware(localAccountValidator)), executeIfActivityPub(asyncMiddleware(localAccountValidator)),
executeIfActivityPub(asyncMiddleware(accountFollowingController)) executeIfActivityPub(asyncMiddleware(accountFollowingController))
) )
activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))),
executeIfActivityPub(getAccountVideoRate('like'))
)
activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))),
executeIfActivityPub(getAccountVideoRate('dislike'))
)
activityPubClientRouter.get('/videos/watch/:id', activityPubClientRouter.get('/videos/watch/:id',
executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))),
@ -62,7 +76,7 @@ activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))),
executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) executeIfActivityPub(asyncMiddleware(videoAnnouncesController))
) )
activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
executeIfActivityPub(asyncMiddleware(videosShareValidator)), executeIfActivityPub(asyncMiddleware(videosShareValidator)),
executeIfActivityPub(asyncMiddleware(videoAnnounceController)) executeIfActivityPub(asyncMiddleware(videoAnnounceController))
) )
@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
return activityPubResponse(activityPubContextify(activityPubResult), res) return activityPubResponse(activityPubContextify(activityPubResult), res)
} }
function getAccountVideoRate (rateType: VideoRateType) {
return (req: express.Request, res: express.Response) => {
const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate
const byActor = accountVideoRate.Account.Actor
const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
const APObject = rateType === 'like'
? buildLikeActivity(url, byActor, accountVideoRate.Video)
: buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video))
return activityPubResponse(activityPubContextify(APObject), res)
}
}
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video const video: VideoModel = res.locals.video
@ -276,7 +304,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
return { return {
total: result.count, total: result.count,
data: result.rows.map(r => r.Account.Actor.url) data: result.rows.map(r => r.url)
} }
} }
return activityPubCollectionPagination(url, handler, req.query.page) return activityPubCollectionPagination(url, handler, req.query.page)

View File

@ -43,11 +43,13 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
processActivities(task.activities, task.signatureActor, task.inboxActor) const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
processActivities(task.activities, options)
.then(() => cb()) .then(() => cb())
}) })
function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { function inboxController (req: express.Request, res: express.Response) {
const rootActivity: RootActivity = req.body const rootActivity: RootActivity = req.body
let activities: Activity[] = [] let activities: Activity[] = []

View File

@ -405,7 +405,11 @@ async function viewVideo (req: express.Request, res: express.Response) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
await sendCreateView(serverActor, videoInstance, undefined) // Send the event to the origin server
// If we own the video, we'll send an update event when we'll process the views (in our job queue)
if (videoInstance.isOwned() === false) {
await sendCreateView(serverActor, videoInstance, undefined)
}
return res.status(204).end() return res.status(204).end()
} }

View File

@ -2,8 +2,8 @@ import * as express from 'express'
import { UserVideoRateUpdate } from '../../../../shared' import { UserVideoRateUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
import { sendVideoRateChange } from '../../../lib/activitypub' import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
rateVideoRouter.put('/:id/rate', rateVideoRouter.put('/:id/rate',
authenticate, authenticate,
asyncMiddleware(videoRateValidator), asyncMiddleware(videoUpdateRateValidator),
asyncRetryTransactionMiddleware(rateVideo) asyncRetryTransactionMiddleware(rateVideo)
) )
@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
const body: UserVideoRateUpdate = req.body const body: UserVideoRateUpdate = req.body
const rateType = body.rating const rateType = body.rating
const videoInstance: VideoModel = res.locals.video const videoInstance: VideoModel = res.locals.video
const userAccount: AccountModel = res.locals.oauth.token.User.Account
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t } const sequelizeOptions = { transaction: t }
const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) const accountInstance = await AccountModel.load(userAccount.id, t)
const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
let likesToIncrement = 0 let likesToIncrement = 0
@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
// There was a previous rate, update it // There was a previous rate, update it
if (previousRate) { if (previousRate) {
// We will remove the previous rate, so we will need to update the video count attribute // We will remove the previous rate, so we will need to update the video count attribute
if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- if (previousRate.type === 'like') likesToIncrement--
else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- else if (previousRate.type === 'dislike') dislikesToIncrement--
if (rateType === 'none') { // Destroy previous rate if (rateType === 'none') { // Destroy previous rate
await previousRate.destroy(sequelizeOptions) await previousRate.destroy(sequelizeOptions)
} else { // Update previous rate } else { // Update previous rate
previousRate.type = rateType previousRate.type = rateType
previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
await previousRate.save(sequelizeOptions) await previousRate.save(sequelizeOptions)
} }
} else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
const query = { const query = {
accountId: accountInstance.id, accountId: accountInstance.id,
videoId: videoInstance.id, videoId: videoInstance.id,
type: rateType type: rateType,
url: getRateUrl(rateType, userAccount.Actor, videoInstance)
} }
await AccountVideoRateModel.create(query, sequelizeOptions) await AccountVideoRateModel.create(query, sequelizeOptions)

View File

@ -6,6 +6,7 @@ import { ACTIVITY_PUB } from '../initializers'
import { ActorModel } from '../models/activitypub/actor' import { ActorModel } from '../models/activitypub/actor'
import { signJsonLDObject } from './peertube-crypto' import { signJsonLDObject } from './peertube-crypto'
import { pageToStartAndCount } from './core-utils' import { pageToStartAndCount } from './core-utils'
import { parse } from 'url'
function activityPubContextify <T> (data: T) { function activityPubContextify <T> (data: T) {
return Object.assign(data, { return Object.assign(data, {
@ -24,7 +25,7 @@ function activityPubContextify <T> (data: T) {
sensitive: 'as:sensitive', sensitive: 'as:sensitive',
language: 'sc:inLanguage', language: 'sc:inLanguage',
views: 'sc:Number', views: 'sc:Number',
stats: 'sc:Number', state: 'sc:Number',
size: 'sc:Number', size: 'sc:Number',
fps: 'sc:Number', fps: 'sc:Number',
commentsEnabled: 'sc:Boolean', commentsEnabled: 'sc:Boolean',
@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
return activityActor.id return activityActor.id
} }
function checkUrlsSameHost (url1: string, url2: string) {
const idHost = parse(url1).host
const actorHost = parse(url2).host
return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
checkUrlsSameHost,
getActorUrl, getActorUrl,
activityPubContextify, activityPubContextify,
activityPubCollectionPagination, activityPubCollectionPagination,

View File

@ -1,5 +1,5 @@
import * as AsyncLRU from 'async-lru' import * as AsyncLRU from 'async-lru'
import * as jsonld from 'jsonld/' import * as jsonld from 'jsonld'
import * as jsig from 'jsonld-signatures' import * as jsig from 'jsonld-signatures'
const nodeDocumentLoader = jsonld.documentLoaders.node() const nodeDocumentLoader = jsonld.documentLoaders.node()
@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
jsig.use('jsonld', jsonld) jsig.use('jsonld', jsonld)
export { jsig } export { jsig, jsonld }

View File

@ -310,6 +310,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
.outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
.outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it
.outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
.outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
.outputOption('-map_metadata -1') // strip all metadata .outputOption('-map_metadata -1') // strip all metadata
.outputOption('-movflags faststart') .outputOption('-movflags faststart')

View File

@ -1,9 +1,12 @@
import { Request } from 'express' import { Request } from 'express'
import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
import { ActorModel } from '../models/activitypub/actor' import { ActorModel } from '../models/activitypub/actor'
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
import { jsig } from './custom-jsonld-signature' import { jsig, jsonld } from './custom-jsonld-signature'
import { logger } from './logger' import { logger } from './logger'
import { cloneDeep } from 'lodash'
import { createVerify } from 'crypto'
import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
const httpSignature = require('http-signature') const httpSignature = require('http-signature')
@ -30,21 +33,36 @@ async function cryptPassword (password: string) {
// HTTP Signature // HTTP Signature
function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) { function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
return buildDigest(rawBody.toString()) === req.headers['digest']
}
return true
}
function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
} }
function parseHTTPSignature (req: Request) { function parseHTTPSignature (req: Request, clockSkew?: number) {
return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME }) return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
} }
// JSONLD // JSONLD
function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) { async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
if (signedDocument.signature.type === 'RsaSignature2017') {
// Mastodon algorithm
const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
// Success? If no, try with our library
if (res === true) return true
}
const publicKeyObject = { const publicKeyObject = {
'@context': jsig.SECURITY_CONTEXT_URL, '@context': jsig.SECURITY_CONTEXT_URL,
id: fromActor.url, id: fromActor.url,
type: 'CryptographicKey', type: 'CryptographicKey',
owner: fromActor.url, owner: fromActor.url,
publicKeyPem: fromActor.publicKey publicKeyPem: fromActor.publicKey
} }
@ -69,6 +87,44 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any)
}) })
} }
// Backward compatibility with "other" implementations
async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
function hash (obj: any): Promise<any> {
return jsonld.promises
.normalize(obj, {
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
.then(res => sha256(res))
}
const signatureCopy = cloneDeep(signedDocument.signature)
Object.assign(signatureCopy, {
'@context': [
'https://w3id.org/security/v1',
{ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
]
})
delete signatureCopy.type
delete signatureCopy.id
delete signatureCopy.signatureValue
const docWithoutSignature = cloneDeep(signedDocument)
delete docWithoutSignature.signature
const [ documentHash, optionsHash ] = await Promise.all([
hash(docWithoutSignature),
hash(signatureCopy)
])
const toVerify = optionsHash + documentHash
const verify = createVerify('RSA-SHA256')
verify.update(toVerify, 'utf8')
return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
}
function signJsonLDObject (byActor: ActorModel, data: any) { function signJsonLDObject (byActor: ActorModel, data: any) {
const options = { const options = {
privateKeyPem: byActor.privateKey, privateKeyPem: byActor.privateKey,
@ -82,6 +138,7 @@ function signJsonLDObject (byActor: ActorModel, data: any) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
isHTTPSignatureDigestValid,
parseHTTPSignature, parseHTTPSignature,
isHTTPSignatureVerified, isHTTPSignatureVerified,
isJsonLDSignatureVerified, isJsonLDSignatureVerified,

View File

@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra'
import * as request from 'request' import * as request from 'request'
import { ACTIVITY_PUB } from '../initializers' import { ACTIVITY_PUB } from '../initializers'
function doRequest ( function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
): Bluebird<{ response: request.RequestResponse, body: any }> { ): Bluebird<{ response: request.RequestResponse, body: any }> {
if (requestOptions.activityPub === true) { if (requestOptions.activityPub === true) {
@ -11,7 +11,7 @@ function doRequest (
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
} }
return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
}) })
} }

View File

@ -16,7 +16,7 @@ let config: IConfig = require('config')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 285 const LAST_MIGRATION_VERSION = 290
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -336,6 +336,9 @@ const CONSTRAINTS_FIELDS = {
VIDEOS_REDUNDANCY: { VIDEOS_REDUNDANCY: {
URL: { min: 3, max: 2000 } // Length URL: { min: 3, max: 2000 } // Length
}, },
VIDEO_RATES: {
URL: { min: 3, max: 2000 } // Length
},
VIDEOS: { VIDEOS: {
NAME: { min: 3, max: 120 }, // Length NAME: { min: 3, max: 120 }, // Length
LANGUAGE: { min: 1, max: 10 }, // Length LANGUAGE: { min: 1, max: 10 }, // Length
@ -535,7 +538,7 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
const HTTP_SIGNATURE = { const HTTP_SIGNATURE = {
HEADER_NAME: 'signature', HEADER_NAME: 'signature',
ALGORITHM: 'rsa-sha256', ALGORITHM: 'rsa-sha256',
HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ] HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ]
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,46 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize,
db: any
}): Promise<void> {
{
const data = {
type: Sequelize.STRING(2000),
allowNull: true
}
await utils.queryInterface.addColumn('accountVideoRate', 'url', data)
}
{
const builtUrlQuery = `SELECT "actor"."url" || '/' || "accountVideoRate"."type" || 's/' || "videoId" ` +
'FROM "accountVideoRate" ' +
'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
'INNER JOIN actor ON actor.id = account."actorId" ' +
'WHERE "base".id = "accountVideoRate".id'
const query = 'UPDATE "accountVideoRate" base SET "url" = (' + builtUrlQuery + ') WHERE "url" IS NULL'
await utils.sequelize.query(query)
}
{
const data = {
type: Sequelize.STRING(2000),
allowNull: false,
defaultValue: null
}
await utils.queryInterface.changeColumn('accountVideoRate', 'url', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -5,7 +5,7 @@ import * as url from 'url'
import * as uuidv4 from 'uuid/v4' import * as uuidv4 from 'uuid/v4'
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
import { getActorUrl } from '../../helpers/activitypub' import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
}
try { try {
// Assert we don't recurse another time // Don't recurse another time
ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
} catch (err) { } catch (err) {
logger.error('Cannot get or create account attributed to video channel ' + actor.url) logger.error('Cannot get or create account attributed to video channel ' + actor.url)
@ -297,12 +301,15 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
normalizeActor(requestResult.body) normalizeActor(requestResult.body)
const actorJSON: ActivityPubActor = requestResult.body const actorJSON: ActivityPubActor = requestResult.body
if (isActorObjectValid(actorJSON) === false) { if (isActorObjectValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
return { result: undefined, statusCode: requestResult.response.statusCode } return { result: undefined, statusCode: requestResult.response.statusCode }
} }
if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
}
const followersCount = await fetchActorTotalItems(actorJSON.followers) const followersCount = await fetchActorTotalItems(actorJSON.followers)
const followingCount = await fetchActorTotalItems(actorJSON.following) const followingCount = await fetchActorTotalItems(actorJSON.following)

View File

@ -2,6 +2,7 @@ import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
import { doRequest } from '../../helpers/requests' import { doRequest } from '../../helpers/requests'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
logger.info('Crawling ActivityPub data on %s.', uri) logger.info('Crawling ActivityPub data on %s.', uri)
@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
timeout: JOB_REQUEST_TIMEOUT timeout: JOB_REQUEST_TIMEOUT
} }
const response = await doRequest(options) const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
const firstBody = response.body const firstBody = response.body
let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
while (nextLink && i < limit) { while (nextLink && i < limit) {
options.uri = nextLink options.uri = nextLink
const { body } = await doRequest(options) const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
nextLink = body.next nextLink = body.next
i++ i++

View File

@ -1,9 +1 @@
export * from './process' export * from './process'
export * from './process-accept'
export * from './process-announce'
export * from './process-create'
export * from './process-delete'
export * from './process-follow'
export * from './process-like'
export * from './process-undo'
export * from './process-update'

View File

@ -12,6 +12,9 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { Redis } from '../../redis' import { Redis } from '../../redis'
import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateCacheFile } from '../cache-file'
import { immutableAssign } from '../../../tests/utils'
import { getVideoDislikeActivityPubUrl } from '../url'
import { VideoModel } from '../../../models/video/video'
async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
const activityObject = activity.object const activityObject = activity.object
@ -65,9 +68,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
videoId: video.id, videoId: video.id,
accountId: byAccount.id accountId: byAccount.id
} }
const [ , created ] = await AccountVideoRateModel.findOrCreate({ const [ , created ] = await AccountVideoRateModel.findOrCreate({
where: rate, where: rate,
defaults: rate, defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
transaction: t transaction: t
}) })
if (created === true) await video.increment('dislikes', { transaction: t }) if (created === true) await video.increment('dislikes', { transaction: t })
@ -84,19 +88,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
const view = activity.object as ViewObject const view = activity.object as ViewObject
const options = { const video = await VideoModel.loadByUrl(view.object)
videoObject: view.object, if (!video || video.isOwned() === false) return
fetchType: 'only-video' as 'only-video'
}
const { video } = await getOrCreateVideoAndAccountAndChannel(options)
await Redis.Instance.addVideoView(video.id) await Redis.Instance.addVideoView(video.id)
if (video.isOwned()) {
// Don't resend the activity to the sender
const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
}
} }
async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) {

View File

@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { immutableAssign } from '../../../tests/utils'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
return retryTransactionWrapper(processLikeVideo, byActor, activity) return retryTransactionWrapper(processLikeVideo, byActor, activity)
@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
} }
const [ , created ] = await AccountVideoRateModel.findOrCreate({ const [ , created ] = await AccountVideoRateModel.findOrCreate({
where: rate, where: rate,
defaults: rate, defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
transaction: t transaction: t
}) })
if (created === true) await video.increment('likes', { transaction: t }) if (created === true) await video.increment('likes', { transaction: t })

View File

@ -55,7 +55,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) {
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t)
if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t }) await rate.destroy({ transaction: t })
@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t)
if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t }) await rate.destroy({ transaction: t })

View File

@ -1,5 +1,5 @@
import { Activity, ActivityType } from '../../../../shared/models/activitypub' import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { getActorUrl } from '../../../helpers/activitypub' import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { processAcceptActivity } from './process-accept' import { processAcceptActivity } from './process-accept'
@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
Like: processLikeActivity Like: processLikeActivity
} }
async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) { async function processActivities (
activities: Activity[],
options: {
signatureActor?: ActorModel
inboxActor?: ActorModel
outboxUrl?: string
} = {}) {
const actorsCache: { [ url: string ]: ActorModel } = {} const actorsCache: { [ url: string ]: ActorModel } = {}
for (const activity of activities) { for (const activity of activities) {
if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) {
logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
continue continue
} }
@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
const actorUrl = getActorUrl(activity.actor) const actorUrl = getActorUrl(activity.actor)
// When we fetch remote data, we don't have signature // When we fetch remote data, we don't have signature
if (signatureActor && actorUrl !== signatureActor.url) { if (options.signatureActor && actorUrl !== options.signatureActor.url) {
logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url) logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
continue continue
} }
const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) {
logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl)
continue
}
const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl)
actorsCache[actorUrl] = byActor actorsCache[actorUrl] = byActor
const activityProcessor = processActivity[activity.type] const activityProcessor = processActivity[activity.type]
@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
} }
try { try {
await activityProcessor(activity, byActor, inboxActor) await activityProcessor(activity, byActor, options.inboxActor)
} catch (err) { } catch (err) {
logger.warn('Cannot process activity %s.', activity.type, { err }) logger.warn('Cannot process activity %s.', activity.type, { err })
} }

View File

@ -95,7 +95,7 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
logger.info('Creating job to send view of %s.', video.url) logger.info('Creating job to send view of %s.', video.url)
const url = getVideoViewActivityPubUrl(byActor, video) const url = getVideoViewActivityPubUrl(byActor, video)
const viewActivity = buildViewActivity(byActor, video) const viewActivity = buildViewActivity(url, byActor, video)
return sendVideoRelatedCreateActivity({ return sendVideoRelatedCreateActivity({
// Use the server actor to send the view // Use the server actor to send the view
@ -111,7 +111,7 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
logger.info('Creating job to dislike %s.', video.url) logger.info('Creating job to dislike %s.', video.url)
const url = getVideoDislikeActivityPubUrl(byActor, video) const url = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = buildDislikeActivity(byActor, video) const dislikeActivity = buildDislikeActivity(url, byActor, video)
return sendVideoRelatedCreateActivity({ return sendVideoRelatedCreateActivity({
byActor, byActor,
@ -136,16 +136,18 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud
) )
} }
function buildDislikeActivity (byActor: ActorModel, video: VideoModel) { function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) {
return { return {
id: url,
type: 'Dislike', type: 'Dislike',
actor: byActor.url, actor: byActor.url,
object: video.url object: video.url
} }
} }
function buildViewActivity (byActor: ActorModel, video: VideoModel) { function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
return { return {
id: url,
type: 'View', type: 'View',
actor: byActor.url, actor: byActor.url,
object: video.url object: video.url

View File

@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel,
return audiencify( return audiencify(
{ {
type: 'Like' as 'Like',
id: url, id: url,
type: 'Like' as 'Like',
actor: byActor.url, actor: byActor.url,
object: video.url object: video.url
}, },

View File

@ -64,7 +64,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
logger.info('Creating job to undo a dislike of video %s.', video.url) logger.info('Creating job to undo a dislike of video %s.', video.url)
const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = buildDislikeActivity(byActor, video) const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t })

View File

@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { sendUndoAnnounce, sendVideoAnnounce } from './send' import { sendUndoAnnounce, sendVideoAnnounce } from './send'
import { getAnnounceActivityPubUrl } from './url' import { getVideoAnnounceActivityPubUrl } from './url'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { doRequest } from '../../helpers/requests' import { doRequest } from '../../helpers/requests'
import { getOrCreateActorAndServerAndModel } from './actor' import { getOrCreateActorAndServerAndModel } from './actor'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
if (video.privacy === VideoPrivacy.PRIVATE) return undefined if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
json: true, json: true,
activityPub: true activityPub: true
}) })
if (!body || !body.actor) throw new Error('Body of body actor is invalid') if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getActorUrl(body.actor)
if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
}
const actorUrl = body.actor
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = { const entry = {
@ -72,7 +77,7 @@ export {
async function shareByServer (video: VideoModel, t: Transaction) { async function shareByServer (video: VideoModel, t: Transaction) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
return VideoShareModel.findOrCreate({ return VideoShareModel.findOrCreate({
defaults: { defaults: {
actorId: serverActor.id, actorId: serverActor.id,
@ -91,7 +96,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
} }
async function shareByVideoChannel (video: VideoModel, t: Transaction) { async function shareByVideoChannel (video: VideoModel, t: Transaction) {
const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
return VideoShareModel.findOrCreate({ return VideoShareModel.findOrCreate({
defaults: { defaults: {
actorId: video.VideoChannel.actorId, actorId: video.VideoChannel.actorId,

View File

@ -33,14 +33,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
} }
function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString() return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
} }
function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
return byActor.url + '/likes/' + video.id return byActor.url + '/likes/' + video.id
} }
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
return byActor.url + '/dislikes/' + video.id return byActor.url + '/dislikes/' + video.id
} }
@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
return follower.url + '/accepts/follows/' + me.id return follower.url + '/accepts/follows/' + me.id
} }
function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) { function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return originalUrl + '/announces/' + byActor.id return video.url + '/announces/' + byActor.id
} }
function getDeleteActivityPubUrl (originalUrl: string) { function getDeleteActivityPubUrl (originalUrl: string) {
@ -97,7 +97,7 @@ export {
getVideoAbuseActivityPubUrl, getVideoAbuseActivityPubUrl,
getActorFollowActivityPubUrl, getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl, getActorFollowAcceptActivityPubUrl,
getAnnounceActivityPubUrl, getVideoAnnounceActivityPubUrl,
getUpdateActivityPubUrl, getUpdateActivityPubUrl,
getUndoActivityPubUrl, getUndoActivityPubUrl,
getVideoViewActivityPubUrl, getVideoViewActivityPubUrl,

View File

@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment'
import { getOrCreateActorAndServerAndModel } from './actor' import { getOrCreateActorAndServerAndModel } from './actor'
import { getOrCreateVideoAndAccountAndChannel } from './videos' import { getOrCreateVideoAndAccountAndChannel } from './videos'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { checkUrlsSameHost } from '../../helpers/activitypub'
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
let originCommentId: number = null let originCommentId: number = null
@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
const actorUrl = body.attributedTo const actorUrl = body.attributedTo
if (!actorUrl) return { created: false } if (!actorUrl) return { created: false }
if (checkUrlsSameHost(commentUrl, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`)
}
if (checkUrlsSameHost(body.id, commentUrl) !== true) {
throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
if (!entry) return { created: false } if (!entry) return { created: false }
@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
const actorUrl = body.attributedTo const actorUrl = body.attributedTo
if (!actorUrl) throw new Error('Miss attributed to in comment') if (!actorUrl) throw new Error('Miss attributed to in comment')
if (checkUrlsSameHost(url, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
}
if (checkUrlsSameHost(body.id, url) !== true) {
throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const comment = new VideoCommentModel({ const comment = new VideoCommentModel({
url: body.id, url: body.id,

View File

@ -8,13 +8,35 @@ import { getOrCreateActorAndServerAndModel } from './actor'
import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { doRequest } from '../../helpers/requests'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
import { ActorModel } from '../../models/activitypub/actor'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
let rateCounts = 0 let rateCounts = 0
await Bluebird.map(actorUrls, async actorUrl => { await Bluebird.map(ratesUrl, async rateUrl => {
try { try {
// Fetch url
const { body } = await doRequest({
uri: rateUrl,
json: true,
activityPub: true
})
if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getActorUrl(body.actor)
if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
}
if (checkUrlsSameHost(body.id, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const [ , created ] = await AccountVideoRateModel const [ , created ] = await AccountVideoRateModel
.findOrCreate({ .findOrCreate({
where: { where: {
@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
defaults: { defaults: {
videoId: video.id, videoId: video.id,
accountId: actor.Account.id, accountId: actor.Account.id,
type: rate type: rate,
url: body.id
} }
}) })
if (created) rateCounts += 1 if (created) rateCounts += 1
} catch (err) { } catch (err) {
logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) logger.warn('Cannot add rate %s.', rateUrl, { err })
} }
}, { concurrency: CRAWL_REQUEST_CONCURRENCY }) }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
if (dislikes > 0) await sendCreateDislike(actor, video, t) if (dislikes > 0) await sendCreateDislike(actor, video, t)
} }
function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video)
}
export { export {
getRateUrl,
createRates, createRates,
sendVideoRateChange sendVideoRateChange
} }

View File

@ -29,6 +29,7 @@ import { createRates } from './video-rates'
import { addVideoShares, shareVideoByServerAndChannel } from './share' import { addVideoShares, shareVideoByServerAndChannel } from './share'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
import { checkUrlsSameHost } from '../../helpers/activitypub'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
// If the video is not private and published, we federate it // If the video is not private and published, we federate it
@ -63,7 +64,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
const { response, body } = await doRequest(options) const { response, body } = await doRequest(options)
if (sanitizeAndCheckVideoTorrentObject(body) === false) { if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
logger.debug('Remote video JSON is not valid.', { body }) logger.debug('Remote video JSON is not valid.', { body })
return { response, videoObject: undefined } return { response, videoObject: undefined }
} }
@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
const channel = videoObject.attributedTo.find(a => a.type === 'Group') const channel = videoObject.attributedTo.find(a => a.type === 'Group')
if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
}
return getOrCreateActorAndServerAndModel(channel.id, 'all') return getOrCreateActorAndServerAndModel(channel.id, 'all')
} }

View File

@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
'activity': items => processActivities(items), 'activity': items => processActivities(items, { outboxUrl: payload.uri }),
'video-likes': items => createRates(items, video, 'like'), 'video-likes': items => createRates(items, video, 'like'),
'video-dislikes': items => createRates(items, video, 'dislike'), 'video-dislikes': items => createRates(items, video, 'dislike'),
'video-shares': items => addVideoShares(items, video), 'video-shares': items => addVideoShares(items, video),

View File

@ -38,15 +38,20 @@ async function buildSignedRequestOptions (payload: Payload) {
} }
} }
function buildGlobalHeaders (body: object) { function buildGlobalHeaders (body: any) {
const digest = 'SHA-256=' + sha256(JSON.stringify(body), 'base64')
return { return {
'Digest': digest 'Digest': buildDigest(body)
} }
} }
function buildDigest (body: any) {
const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
return 'SHA-256=' + sha256(rawBody, 'base64')
}
export { export {
buildDigest,
buildGlobalHeaders, buildGlobalHeaders,
computeBody, computeBody,
buildSignedRequestOptions buildSignedRequestOptions

View File

@ -3,8 +3,9 @@ import { logger } from '../../../helpers/logger'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoViewModel } from '../../../models/video/video-views' import { VideoViewModel } from '../../../models/video/video-views'
import { isTestInstance } from '../../../helpers/core-utils' import { isTestInstance } from '../../../helpers/core-utils'
import { federateVideoIfNeeded } from '../../activitypub'
async function processVideosViewsViews () { async function processVideosViews () {
const lastHour = new Date() const lastHour = new Date()
// In test mode, we run this function multiple times per hour, so we don't want the values of the previous hour // In test mode, we run this function multiple times per hour, so we don't want the values of the previous hour
@ -36,6 +37,9 @@ async function processVideosViewsViews () {
views, views,
videoId videoId
}) })
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
if (video.isOwned()) await federateVideoIfNeeded(video, false)
} catch (err) { } catch (err) {
logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour) logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour)
} }
@ -51,5 +55,5 @@ async function processVideosViewsViews () {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
processVideosViewsViews processVideosViews
} }

View File

@ -10,7 +10,7 @@ import { EmailPayload, processEmail } from './handlers/email'
import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file' import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file'
import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
import { processVideoImport, VideoImportPayload } from './handlers/video-import' import { processVideoImport, VideoImportPayload } from './handlers/video-import'
import { processVideosViewsViews } from './handlers/video-views' import { processVideosViews } from './handlers/video-views'
type CreateJobArgument = type CreateJobArgument =
{ type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@ -32,7 +32,7 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
'video-file': processVideoFile, 'video-file': processVideoFile,
'email': processEmail, 'email': processEmail,
'video-import': processVideoImport, 'video-import': processVideoImport,
'videos-views': processVideosViewsViews 'videos-views': processVideosViews
} }
const jobTypes: JobType[] = [ const jobTypes: JobType[] = [

View File

@ -185,11 +185,12 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
} }
private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) {
const maxSize = redundancy.size - this.getTotalFileSizes(filesToDuplicate) const maxSize = redundancy.size
const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy) const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy)
const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(filesToDuplicate)
return totalDuplicated > maxSize return totalWillDuplicate > maxSize
} }
private buildNewExpiration (expiresAfterMs: number) { private buildNewExpiration (expiresAfterMs: number) {

View File

@ -53,7 +53,8 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
export { export {
checkSignature, checkSignature,
executeIfActivityPub executeIfActivityPub,
checkHttpSignature
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -94,7 +95,7 @@ async function checkHttpSignature (req: Request, res: Response) {
async function checkJsonLDSignature (req: Request, res: Response) { async function checkJsonLDSignature (req: Request, res: Response) {
const signatureObject: ActivityPubSignature = req.body.signature const signatureObject: ActivityPubSignature = req.body.signature
if (!signatureObject.creator) { if (!signatureObject || !signatureObject.creator) {
res.sendStatus(403) res.sendStatus(403)
return false return false
} }

View File

@ -5,4 +5,6 @@ export * from './video-channels'
export * from './video-comments' export * from './video-comments'
export * from './video-imports' export * from './video-imports'
export * from './video-watch' export * from './video-watch'
export * from './video-rates'
export * from './video-shares'
export * from './videos' export * from './videos'

View File

@ -0,0 +1,55 @@
import * as express from 'express'
import 'express-validator'
import { body, param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { areValidationErrors } from '../utils'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoRateType } from '../../../../shared/models/videos'
import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
const videoUpdateRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoRate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const getAccountVideoRateValidator = function (rateType: VideoRateType) {
return [
param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
if (areValidationErrors(req, res)) return
const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId)
if (!rate) {
return res.status(404)
.json({ error: 'Video rate not found' })
.end()
}
res.locals.accountVideoRate = rate
return next()
}
]
}
// ---------------------------------------------------------------------------
export {
videoUpdateRateValidator,
getAccountVideoRateValidator
}

View File

@ -0,0 +1,38 @@
import * as express from 'express'
import 'express-validator'
import { param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
import { isVideoExist } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { VideoShareModel } from '../../../models/video/video-share'
import { areValidationErrors } from '../utils'
import { VideoModel } from '../../../models/video/video'
const videosShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const video: VideoModel = res.locals.video
const share = await VideoShareModel.load(req.params.actorId, video.id)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoShare = share
return next()
}
]
// ---------------------------------------------------------------------------
export {
videosShareValidator
}

View File

@ -26,14 +26,12 @@ import {
isVideoLicenceValid, isVideoLicenceValid,
isVideoNameValid, isVideoNameValid,
isVideoPrivacyValid, isVideoPrivacyValid,
isVideoRatingTypeValid,
isVideoSupportValid, isVideoSupportValid,
isVideoTagsValid isVideoTagsValid
} from '../../../helpers/custom-validators/videos' } from '../../../helpers/custom-validators/videos'
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { CONSTRAINTS_FIELDS } from '../../../initializers' import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { VideoShareModel } from '../../../models/video/video-share'
import { authenticate } from '../../oauth' import { authenticate } from '../../oauth'
import { areValidationErrors } from '../utils' import { areValidationErrors } from '../utils'
import { cleanUpReqFiles } from '../../../helpers/express-utils' import { cleanUpReqFiles } from '../../../helpers/express-utils'
@ -188,41 +186,6 @@ const videosRemoveValidator = [
} }
] ]
const videoRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoRate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const videosShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoShare = share
return next()
}
]
const videosChangeOwnershipValidator = [ const videosChangeOwnershipValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@ -415,9 +378,6 @@ export {
videosGetValidator, videosGetValidator,
videosCustomGetValidator, videosCustomGetValidator,
videosRemoveValidator, videosRemoveValidator,
videosShareValidator,
videoRateValidator,
videosChangeOwnershipValidator, videosChangeOwnershipValidator,
videosTerminateChangeOwnershipValidator, videosTerminateChangeOwnershipValidator,

View File

@ -1,12 +1,14 @@
import { values } from 'lodash' import { values } from 'lodash'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
import { VideoRateType } from '../../../shared/models/videos' import { VideoRateType } from '../../../shared/models/videos'
import { VIDEO_RATE_TYPES } from '../../initializers' import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
import { AccountModel } from './account' import { AccountModel } from './account'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
/* /*
Account rates per video. Account rates per video.
@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
}, },
{ {
fields: [ 'videoId', 'type' ] fields: [ 'videoId', 'type' ]
},
{
fields: [ 'url' ],
unique: true
} }
] ]
}) })
@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
@Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
type: VideoRateType type: VideoRateType
@AllowNull(false)
@Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
url: string
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
}) })
Account: AccountModel Account: AccountModel
static load (accountId: number, videoId: number, transaction: Transaction) { static load (accountId: number, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = { const options: IFindOptions<AccountVideoRateModel> = {
where: { where: {
accountId, accountId,
@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
return AccountVideoRateModel.findOne(options) return AccountVideoRateModel.findOne(options)
} }
static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
videoId,
type: rateType
},
include: [
{
model: AccountModel.unscoped(),
required: true,
include: [
{
attributes: [ 'id', 'url', 'preferredUsername' ],
model: ActorModel.unscoped(),
required: true,
where: {
preferredUsername: accountName
}
}
]
},
{
model: VideoModel.unscoped(),
required: true
}
]
}
if (transaction) options.transaction = transaction
return AccountVideoRateModel.findOne(options)
}
static loadByUrl (url: string, transaction: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
url
}
}
if (transaction) options.transaction = transaction
return AccountVideoRateModel.findOne(options)
}
static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) {
const query = { const query = {
offset: start, offset: start,

View File

@ -47,7 +47,7 @@ enum ScopeNames {
required: true, required: true,
include: [ include: [
{ {
attributes: [ 'id' ], attributes: [ 'id', 'url' ],
model: () => ActorModel.unscoped(), model: () => ActorModel.unscoped(),
required: true required: true
} }

View File

@ -293,6 +293,11 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
} }
return VideoFileModel.sum('size', options as any) // FIXME: typings return VideoFileModel.sum('size', options as any) // FIXME: typings
.then(v => {
if (!v || isNaN(v)) return 0
return v
})
} }
static async listLocalExpired () { static async listLocalExpired () {

View File

@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
}) })
Video: VideoModel Video: VideoModel
static load (actorId: number, videoId: number, t: Sequelize.Transaction) { static load (actorId: number, videoId: number, t?: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
where: { where: {
actorId, actorId,

View File

@ -11,6 +11,7 @@ import {
setAccessTokensToServers setAccessTokensToServers
} from '../../shared/utils' } from '../../shared/utils'
const expect = chai.expect const expect = chai.expect
describe('Test activitypub', function () { describe('Test activitypub', function () {

View File

@ -0,0 +1,86 @@
/* tslint:disable:no-unused-expression */
import 'mocha'
import {
createUser,
doubleFollow,
flushAndRunMultipleServers,
flushTests,
getVideosListSort,
killallServers,
ServerInfo,
setAccessTokensToServers,
uploadVideo,
userLogin
} from '../../utils'
import * as chai from 'chai'
import { setActorField, setVideoField } from '../../utils/miscs/sql'
import { waitJobs } from '../../utils/server/jobs'
import { Video } from '../../../../shared/models/videos'
const expect = chai.expect
describe('Test ActivityPub fetcher', function () {
let servers: ServerInfo[]
// ---------------------------------------------------------------
before(async function () {
this.timeout(60000)
servers = await flushAndRunMultipleServers(3)
// Get the access tokens
await setAccessTokensToServers(servers)
const user = { username: 'user1', password: 'password' }
for (const server of servers) {
await createUser(server.url, server.accessToken, user.username, user.password)
}
const userAccessToken = await userLogin(servers[0], user)
await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video root' })
const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'bad video root' })
const badVideoUUID = res.body.video.uuid
await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' })
await setActorField(1, 'http://localhost:9001/accounts/user1', 'url', 'http://localhost:9002/accounts/user1')
await setVideoField(1, badVideoUUID, 'url', 'http://localhost:9003/videos/watch/' + badVideoUUID)
})
it('Should add only the video with a valid actor URL', async function () {
this.timeout(60000)
await doubleFollow(servers[0], servers[1])
await waitJobs(servers)
{
const res = await getVideosListSort(servers[0].url, 'createdAt')
expect(res.body.total).to.equal(3)
const data: Video[] = res.body.data
expect(data[0].name).to.equal('video root')
expect(data[1].name).to.equal('bad video root')
expect(data[2].name).to.equal('video user')
}
{
const res = await getVideosListSort(servers[1].url, 'createdAt')
expect(res.body.total).to.equal(1)
const data: Video[] = res.body.data
expect(data[0].name).to.equal('video root')
}
})
after(async function () {
killallServers(servers)
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -0,0 +1,182 @@
/* tslint:disable:no-unused-expression */
import 'mocha'
import { expect } from 'chai'
import { buildRequestStub } from '../../utils'
import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
import { cloneDeep } from 'lodash'
import { buildSignedActivity } from '../../../helpers/activitypub'
describe('Test activity pub helpers', function () {
describe('When checking the Linked Signature', function () {
it('Should fail with an invalid Mastodon signature', async function () {
const body = require('./json/mastodon/create-bad-signature.json')
const publicKey = require('./json/mastodon/public-key.json').publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
expect(result).to.be.false
})
it('Should fail with an invalid public key', async function () {
const body = require('./json/mastodon/create.json')
const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
expect(result).to.be.false
})
it('Should succeed with a valid Mastodon signature', async function () {
const body = require('./json/mastodon/create.json')
const publicKey = require('./json/mastodon/public-key.json').publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
expect(result).to.be.true
})
it('Should fail with an invalid PeerTube signature', async function () {
const keys = require('./json/peertube/invalid-keys.json')
const body = require('./json/peertube/announce-without-context.json')
const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
const signedBody = await buildSignedActivity(actorSignature as any, body)
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
expect(result).to.be.false
})
it('Should fail with an invalid PeerTube URL', async function () {
const keys = require('./json/peertube/keys.json')
const body = require('./json/peertube/announce-without-context.json')
const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
const signedBody = await buildSignedActivity(actorSignature as any, body)
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
expect(result).to.be.false
})
it('Should succeed with a valid PeerTube signature', async function () {
const keys = require('./json/peertube/keys.json')
const body = require('./json/peertube/announce-without-context.json')
const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
const signedBody = await buildSignedActivity(actorSignature as any, body)
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
expect(result).to.be.true
})
})
describe('When checking HTTP signature', function () {
it('Should fail with an invalid http signature', async function () {
const req = buildRequestStub()
req.method = 'POST'
req.url = '/accounts/ronan/inbox'
const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json'))
req.body = mastodonObject.body
req.headers = mastodonObject.headers
req.headers.signature = 'Signature ' + req.headers.signature
const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
const publicKey = require('./json/mastodon/public-key.json').publicKey
const actor = { publicKey }
const verified = isHTTPSignatureVerified(parsed, actor as any)
expect(verified).to.be.false
})
it('Should fail with an invalid public key', async function () {
const req = buildRequestStub()
req.method = 'POST'
req.url = '/accounts/ronan/inbox'
const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
req.body = mastodonObject.body
req.headers = mastodonObject.headers
req.headers.signature = 'Signature ' + req.headers.signature
const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
const actor = { publicKey }
const verified = isHTTPSignatureVerified(parsed, actor as any)
expect(verified).to.be.false
})
it('Should fail because of clock skew', async function () {
const req = buildRequestStub()
req.method = 'POST'
req.url = '/accounts/ronan/inbox'
const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
req.body = mastodonObject.body
req.headers = mastodonObject.headers
req.headers.signature = 'Signature ' + req.headers.signature
let errored = false
try {
parseHTTPSignature(req)
} catch {
errored = true
}
expect(errored).to.be.true
})
it('Should fail without scheme', async function () {
const req = buildRequestStub()
req.method = 'POST'
req.url = '/accounts/ronan/inbox'
const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
req.body = mastodonObject.body
req.headers = mastodonObject.headers
let errored = false
try {
parseHTTPSignature(req, 3600 * 365 * 3)
} catch {
errored = true
}
expect(errored).to.be.true
})
it('Should succeed with a valid signature', async function () {
const req = buildRequestStub()
req.method = 'POST'
req.url = '/accounts/ronan/inbox'
const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
req.body = mastodonObject.body
req.headers = mastodonObject.headers
req.headers.signature = 'Signature ' + req.headers.signature
const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
const publicKey = require('./json/mastodon/public-key.json').publicKey
const actor = { publicKey }
const verified = isHTTPSignatureVerified(parsed, actor as any)
expect(verified).to.be.true
})
})
})

View File

@ -0,0 +1,4 @@
import './client'
import './fetch'
import './helpers'
import './security'

View File

@ -0,0 +1,93 @@
{
"headers": {
"user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
"host": "localhost",
"date": "Mon, 22 Oct 2018 13:34:22 GMT",
"accept-encoding": "gzip",
"digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
"content-type": "application/activity+json",
"signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
"content-length": "2815"
},
"body": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value"
}
],
"id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
"type": "Create",
"actor": "http://localhost:3000/users/ronan2",
"published": "2018-10-22T13:34:18Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"object": {
"id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
"type": "Note",
"summary": null,
"inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"published": "2018-10-22T13:34:18Z",
"url": "http://localhost:3000/@ronan2/100939547203370948",
"attributedTo": "http://localhost:3000/users/ronan2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"sensitive": false,
"atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
"inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
"content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "http://localhost:9000/accounts/ronan",
"name": "@ronan@localhost:9000"
}
]
},
"signature": {
"type": "RsaSignature2017",
"creator": "http://localhost:3000/users/ronan2#main-key",
"created": "2018-10-22T13:34:19Z",
"signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
}
}
}

View File

@ -0,0 +1,93 @@
{
"headers": {
"user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
"host": "localhost",
"date": "Mon, 22 Oct 2018 13:34:22 GMT",
"accept-encoding": "gzip",
"digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
"content-type": "application/activity+json",
"signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl4wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
"content-length": "2815"
},
"body": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value"
}
],
"id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
"type": "Create",
"actor": "http://localhost:3000/users/ronan2",
"published": "2018-10-22T13:34:18Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"object": {
"id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
"type": "Note",
"summary": null,
"inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"published": "2018-10-22T13:34:18Z",
"url": "http://localhost:3000/@ronan2/100939547203370948",
"attributedTo": "http://localhost:3000/users/ronan2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"sensitive": false,
"atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
"inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
"content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "http://localhost:9000/accounts/ronan",
"name": "@ronan@localhost:9000"
}
]
},
"signature": {
"type": "RsaSignature2017",
"creator": "http://localhost:3000/users/ronan2#main-key",
"created": "2018-10-22T13:34:19Z",
"signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
}
}
}

View File

@ -0,0 +1,3 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl77j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
}

View File

@ -0,0 +1,81 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value"
}
],
"id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
"type": "Create",
"actor": "http://localhost:3000/users/ronan2",
"published": "2018-10-22T12:43:07Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"object": {
"id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
"type": "Note",
"summary": null,
"inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"published": "2018-10-22T12:43:07Z",
"url": "http://localhost:3000/@ronan2/100939345950887698",
"attributedTo": "http://localhost:3000/users/ronan2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"sensitive": false,
"atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
"inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
"content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "http://localhost:9000/accounts/ronan",
"name": "@ronan@localhost:9000"
}
]
},
"signature": {
"type": "RsaSignature2017",
"creator": "http://localhost:3000/users/ronan2#main-key",
"created": "2018-10-22T12:43:08Z",
"signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
}
}

View File

@ -0,0 +1,81 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value"
}
],
"id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
"type": "Create",
"actor": "http://localhost:3000/users/ronan2",
"published": "2018-10-22T12:43:07Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"object": {
"id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
"type": "Note",
"summary": null,
"inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"published": "2018-10-22T12:43:07Z",
"url": "http://localhost:3000/@ronan2/100939345950887698",
"attributedTo": "http://localhost:3000/users/ronan2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"sensitive": false,
"atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
"inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
"content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "http://localhost:9000/accounts/ronan",
"name": "@ronan@localhost:9000"
}
]
},
"signature": {
"type": "RsaSignature2017",
"creator": "http://localhost:3000/users/ronan2#main-key",
"created": "2018-10-22T12:43:08Z",
"signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
}
}

View File

@ -0,0 +1,93 @@
{
"headers": {
"user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
"host": "localhost",
"date": "Mon, 22 Oct 2018 13:34:22 GMT",
"accept-encoding": "gzip",
"digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
"content-type": "application/activity+json",
"signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
"content-length": "2815"
},
"body": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value"
}
],
"id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
"type": "Create",
"actor": "http://localhost:3000/users/ronan2",
"published": "2018-10-22T13:34:18Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"object": {
"id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
"type": "Note",
"summary": null,
"inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"published": "2018-10-22T13:34:18Z",
"url": "http://localhost:3000/@ronan2/100939547203370948",
"attributedTo": "http://localhost:3000/users/ronan2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"http://localhost:3000/users/ronan2/followers",
"http://localhost:9000/accounts/ronan"
],
"sensitive": false,
"atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
"inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
"conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
"content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "http://localhost:9000/accounts/ronan",
"name": "@ronan@localhost:9000"
}
]
},
"signature": {
"type": "RsaSignature2017",
"creator": "http://localhost:3000/users/ronan2#main-key",
"created": "2018-10-22T13:34:19Z",
"signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
}
}
}

View File

@ -0,0 +1,3 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl87j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
}

View File

@ -0,0 +1,13 @@
{
"type": "Announce",
"id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1",
"actor": "http://localhost:9002/accounts/peertube",
"object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"http://localhost:9002/accounts/peertube/followers",
"http://localhost:9002/video-channels/root_channel/followers",
"http://localhost:9002/accounts/root/followers"
],
"cc": []
}

View File

@ -0,0 +1,6 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
}

View File

@ -0,0 +1,4 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
}

View File

@ -0,0 +1,180 @@
/* tslint:disable:no-unused-expression */
import 'mocha'
import { flushAndRunMultipleServers, flushTests, killallServers, makePOSTAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
import { HTTP_SIGNATURE } from '../../../initializers'
import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
import * as chai from 'chai'
import { setActorField } from '../../utils/miscs/sql'
import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
const expect = chai.expect
function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) {
return Promise.all([
setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey),
setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey)
])
}
function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) {
return Promise.all([
setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey),
setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey)
])
}
describe('Test ActivityPub security', function () {
let servers: ServerInfo[]
let url: string
const keys = require('./json/peertube/keys.json')
const invalidKeys = require('./json/peertube/invalid-keys.json')
const baseHttpSignature = {
algorithm: HTTP_SIGNATURE.ALGORITHM,
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
keyId: 'acct:peertube@localhost:9002',
key: keys.privateKey,
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
}
// ---------------------------------------------------------------
before(async function () {
this.timeout(60000)
servers = await flushAndRunMultipleServers(3)
url = servers[0].url + '/inbox'
await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
const to = { url: 'http://localhost:9001/accounts/peertube' }
const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
await makeFollowRequest(to, by)
})
describe('When checking HTTP signature', function () {
it('Should fail with an invalid digest', async function () {
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = {
Digest: buildDigest({ hello: 'coucou' })
}
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
it('Should fail with an invalid date', async function () {
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body)
headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
it('Should fail with bad keys', async function () {
await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey)
await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey)
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body)
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
it('Should succeed with a valid HTTP signature', async function () {
await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
await setKeysOfServer2(2, keys.publicKey, keys.privateKey)
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body)
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(204)
})
})
describe('When checking Linked Data Signature', function () {
before(async () => {
await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
const to = { url: 'http://localhost:9001/accounts/peertube' }
const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey }
await makeFollowRequest(to, by)
})
it('Should fail with bad keys', async function () {
this.timeout(10000)
await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey)
await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey)
const body = require('./json/peertube/announce-without-context.json')
body.actor = 'http://localhost:9003/accounts/peertube'
const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
const signedBody = await buildSignedActivity(signer, body)
const headers = buildGlobalHeaders(signedBody)
const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
it('Should fail with an altered body', async function () {
this.timeout(10000)
await setKeysOfServer3(1, keys.publicKey, keys.privateKey)
await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
const body = require('./json/peertube/announce-without-context.json')
body.actor = 'http://localhost:9003/accounts/peertube'
const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
const signedBody = await buildSignedActivity(signer, body)
signedBody.actor = 'http://localhost:9003/account/peertube'
const headers = buildGlobalHeaders(signedBody)
const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
it('Should succeed with a valid signature', async function () {
this.timeout(10000)
const body = require('./json/peertube/announce-without-context.json')
body.actor = 'http://localhost:9003/accounts/peertube'
const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
const signedBody = await buildSignedActivity(signer, body)
const headers = buildGlobalHeaders(signedBody)
const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(204)
})
})
after(async function () {
killallServers(servers)
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -1 +1,2 @@
import './redundancy' import './redundancy'
import './activitypub'

View File

@ -54,7 +54,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams:
immutableAssign({ immutableAssign({
min_lifetime: '1 hour', min_lifetime: '1 hour',
strategy: strategy, strategy: strategy,
size: '100KB' size: '200KB'
}, additionalParams) }, additionalParams)
] ]
} }
@ -111,8 +111,8 @@ async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
const stat = data.videosRedundancy[0] const stat = data.videosRedundancy[0]
expect(stat.strategy).to.equal(strategy) expect(stat.strategy).to.equal(strategy)
expect(stat.totalSize).to.equal(102400) expect(stat.totalSize).to.equal(204800)
expect(stat.totalUsed).to.be.at.least(1).and.below(102401) expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
expect(stat.totalVideoFiles).to.equal(4) expect(stat.totalVideoFiles).to.equal(4)
expect(stat.totalVideos).to.equal(1) expect(stat.totalVideos).to.equal(1)
} }
@ -125,7 +125,7 @@ async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
const stat = data.videosRedundancy[0] const stat = data.videosRedundancy[0]
expect(stat.strategy).to.equal(strategy) expect(stat.strategy).to.equal(strategy)
expect(stat.totalSize).to.equal(102400) expect(stat.totalSize).to.equal(204800)
expect(stat.totalUsed).to.equal(0) expect(stat.totalUsed).to.equal(0)
expect(stat.totalVideoFiles).to.equal(0) expect(stat.totalVideoFiles).to.equal(0)
expect(stat.totalVideos).to.equal(0) expect(stat.totalVideos).to.equal(0)
@ -223,7 +223,7 @@ describe('Test videos redundancy', function () {
return enableRedundancyOnServer1() return enableRedundancyOnServer1()
}) })
it('Should have 2 webseed on the first video', async function () { it('Should have 2 webseeds on the first video', async function () {
this.timeout(40000) this.timeout(40000)
await waitJobs(servers) await waitJobs(servers)
@ -270,7 +270,7 @@ describe('Test videos redundancy', function () {
return enableRedundancyOnServer1() return enableRedundancyOnServer1()
}) })
it('Should have 2 webseed on the first video', async function () { it('Should have 2 webseeds on the first video', async function () {
this.timeout(40000) this.timeout(40000)
await waitJobs(servers) await waitJobs(servers)
@ -338,7 +338,7 @@ describe('Test videos redundancy', function () {
await waitJobs(servers) await waitJobs(servers)
}) })
it('Should have 2 webseed on the first video', async function () { it('Should have 2 webseeds on the first video', async function () {
this.timeout(40000) this.timeout(40000)
await waitJobs(servers) await waitJobs(servers)
@ -419,7 +419,7 @@ describe('Test videos redundancy', function () {
killallServers([ servers[0] ]) killallServers([ servers[0] ])
await wait(10000) await wait(15000)
await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
}) })
@ -451,27 +451,23 @@ describe('Test videos redundancy', function () {
video2Server2UUID = res.body.video.uuid video2Server2UUID = res.body.video.uuid
}) })
it('Should cache video 2 webseed on the first video', async function () { it('Should cache video 2 webseeds on the first video', async function () {
this.timeout(50000) this.timeout(120000)
await waitJobs(servers) await waitJobs(servers)
await wait(7000) let checked = false
try { while (checked === false) {
await check1WebSeed(strategy, video1Server2UUID) await wait(1000)
await check2Webseeds(strategy, video2Server2UUID)
} catch {
await wait(3000)
try { try {
await check1WebSeed(strategy, video1Server2UUID) await check1WebSeed(strategy, video1Server2UUID)
await check2Webseeds(strategy, video2Server2UUID) await check2Webseeds(strategy, video2Server2UUID)
} catch {
await wait(5000)
await check1WebSeed(strategy, video1Server2UUID) checked = true
await check2Webseeds(strategy, video2Server2UUID) } catch {
checked = false
} }
} }
}) })

View File

@ -5,6 +5,7 @@ import 'mocha'
import { JobState, Video } from '../../../../shared/models' import { JobState, Video } from '../../../../shared/models'
import { VideoPrivacy } from '../../../../shared/models/videos' import { VideoPrivacy } from '../../../../shared/models/videos'
import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
import { import {
completeVideoCheck, completeVideoCheck,
getVideo, getVideo,
@ -18,6 +19,7 @@ import {
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers,
uploadVideo, uploadVideo,
updateVideo,
wait wait
} from '../../../../shared/utils' } from '../../../../shared/utils'
import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows' import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
@ -199,15 +201,15 @@ describe('Test handle downs', function () {
expect(res.body.data).to.have.lengthOf(2) expect(res.body.data).to.have.lengthOf(2)
}) })
it('Should send a view to server 3, and automatically fetch the video', async function () { it('Should send an update to server 3, and automatically fetch the video', async function () {
this.timeout(15000) this.timeout(15000)
const res1 = await getVideosList(servers[2].url) const res1 = await getVideosList(servers[2].url)
expect(res1.body.data).to.be.an('array') expect(res1.body.data).to.be.an('array')
expect(res1.body.data).to.have.lengthOf(11) expect(res1.body.data).to.have.lengthOf(11)
await viewVideo(servers[0].url, missedVideo1.uuid) await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, { })
await viewVideo(servers[0].url, unlistedVideo.uuid) await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, { })
await waitJobs(servers) await waitJobs(servers)

View File

@ -6,3 +6,4 @@ import './jobs'
import './reverse-proxy' import './reverse-proxy'
import './stats' import './stats'
import './tracker' import './tracker'
import './no-client'

View File

@ -0,0 +1,36 @@
import 'mocha'
import * as request from 'supertest'
import {
flushTests,
killallServers,
ServerInfo
} from '../../utils/index'
import { runServer } from '../../utils/server/servers'
describe('Start and stop server without web client routes', function () {
let server: ServerInfo
before(async function () {
this.timeout(30000)
await flushTests()
server = await runServer(1, {}, ['--no-client'])
})
it('Should fail getting the client', function () {
const req = request(server.url)
.get('/')
return req.expect(404)
})
after(async function () {
killallServers([ server ])
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -44,6 +44,8 @@ describe('Test CLI wrapper', function () {
}) })
after(async function () { after(async function () {
this.timeout(10000)
await execCLI(cmd + ` auth del ${server.url}`) await execCLI(cmd + ` auth del ${server.url}`)
killallServers([ server ]) killallServers([ server ])

View File

@ -1,6 +1,5 @@
// Order of the tests we want to execute // Order of the tests we want to execute
import './client' import './client'
import './activitypub'
import './feeds/' import './feeds/'
import './cli/' import './cli/'
import './api/' import './api/'

View File

@ -0,0 +1,38 @@
import * as Sequelize from 'sequelize'
function getSequelize (serverNumber: number) {
const dbname = 'peertube_test' + serverNumber
const username = 'peertube'
const password = 'peertube'
const host = 'localhost'
const port = 5432
return new Sequelize(dbname, username, password, {
dialect: 'postgres',
host,
port,
operatorsAliases: false,
logging: false
})
}
function setActorField (serverNumber: number, to: string, field: string, value: string) {
const seq = getSequelize(serverNumber)
const options = { type: Sequelize.QueryTypes.UPDATE }
return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
}
function setVideoField (serverNumber: number, uuid: string, field: string, value: string) {
const seq = getSequelize(serverNumber)
const options = { type: Sequelize.QueryTypes.UPDATE }
return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
}
export {
setVideoField,
setActorField
}

View File

@ -0,0 +1,14 @@
function buildRequestStub (): any {
return { }
}
function buildResponseStub (): any {
return {
locals: {}
}
}
export {
buildResponseStub,
buildRequestStub
}

View File

@ -0,0 +1,43 @@
import { doRequest } from '../../../helpers/requests'
import { HTTP_SIGNATURE } from '../../../initializers'
import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
import { activityPubContextify } from '../../../helpers/activitypub'
function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
const options = {
method: 'POST',
uri: url,
json: body,
httpSignature,
headers
}
return doRequest(options)
}
async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
const follow = {
type: 'Follow',
id: by.url + '/toto',
actor: by.url,
object: to.url
}
const body = activityPubContextify(follow)
const httpSignature = {
algorithm: HTTP_SIGNATURE.ALGORITHM,
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
keyId: by.url,
key: by.privateKey,
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
}
const headers = buildGlobalHeaders(body)
return makePOSTAPRequest(to.url, body, httpSignature, headers)
}
export {
makePOSTAPRequest,
makeFollowRequest
}

View File

@ -5,34 +5,25 @@ import { getSettings, writeSettings, netrc } from './cli'
import { isHostValid } from '../helpers/custom-validators/servers' import { isHostValid } from '../helpers/custom-validators/servers'
import { isUserUsernameValid } from '../helpers/custom-validators/users' import { isUserUsernameValid } from '../helpers/custom-validators/users'
function delInstance (url: string) { async function delInstance (url: string) {
return new Promise((res, rej): void => { const settings = await getSettings()
getSettings()
.then(async (settings) => { settings.remotes.splice(settings.remotes.indexOf(url))
settings.remotes.splice(settings.remotes.indexOf(url)) await writeSettings(settings)
await writeSettings(settings)
delete netrc.machines[url] delete netrc.machines[url]
netrc.save() await netrc.save()
res()
})
.catch(err => rej(err))
})
} }
async function setInstance (url: string, username: string, password: string) { async function setInstance (url: string, username: string, password: string) {
return new Promise((res, rej): void => { const settings = await getSettings()
getSettings() if (settings.remotes.indexOf(url) === -1) {
.then(async settings => { settings.remotes.push(url)
if (settings.remotes.indexOf(url) === -1) { }
settings.remotes.push(url) await writeSettings(settings)
}
await writeSettings(settings) netrc.machines[url] = { login: username, password }
netrc.machines[url] = { login: username, password } await netrc.save()
netrc.save()
res()
})
.catch(err => rej(err))
})
} }
function isURLaPeerTubeInstance (url: string) { function isURLaPeerTubeInstance (url: string) {
@ -71,56 +62,60 @@ program
required: true required: true
} }
} }
}, (_, result) => { }, async (_, result) => {
setInstance(result.url, result.username, result.password) await setInstance(result.url, result.username, result.password)
process.exit(0)
}) })
}) })
program program
.command('del <url>') .command('del <url>')
.description('unregisters a remote instance') .description('unregisters a remote instance')
.action((url) => { .action(async url => {
delInstance(url) await delInstance(url)
process.exit(0)
}) })
program program
.command('list') .command('list')
.description('lists registered remote instances') .description('lists registered remote instances')
.action(() => { .action(async () => {
getSettings() const settings = await getSettings()
.then(settings => { const table = new Table({
const table = new Table({ head: ['instance', 'login'],
head: ['instance', 'login'], colWidths: [30, 30]
colWidths: [30, 30] })
}) netrc.loadSync()
netrc.loadSync() settings.remotes.forEach(element => {
settings.remotes.forEach(element => { table.push([
table.push([ element,
element, netrc.machines[element].login
netrc.machines[element].login ])
]) })
})
console.log(table.toString()) console.log(table.toString())
})
process.exit(0)
}) })
program program
.command('set-default <url>') .command('set-default <url>')
.description('set an existing entry as default') .description('set an existing entry as default')
.action((url) => { .action(async url => {
getSettings() const settings = await getSettings()
.then(settings => { const instanceExists = settings.remotes.indexOf(url) !== -1
const instanceExists = settings.remotes.indexOf(url) !== -1
if (instanceExists) { if (instanceExists) {
settings.default = settings.remotes.indexOf(url) settings.default = settings.remotes.indexOf(url)
writeSettings(settings) await writeSettings(settings)
} else {
console.log('<url> is not a registered instance.') process.exit(0)
process.exit(-1) } else {
} console.log('<url> is not a registered instance.')
}) process.exit(-1)
}
}) })
program.on('--help', function () { program.on('--help', function () {

View File

@ -58,7 +58,7 @@ if (!process.argv.slice(2).length) {
,"\\/ ,"\\/
_,.__/"\\/_ (the CLI for red chocobos) _,.__/"\\/_ (the CLI for red chocobos)
/ \\) "./, ". / \\) "./, ".
--/---"---" "-) )---- by Chocobozzz et al.`) --/---"---" "-) )---- by Chocobozzz et al.\n`)
} }
getSettings() getSettings()

View File

@ -1,5 +1,6 @@
export interface DislikeObject { export interface DislikeObject {
type: 'Dislike', id: string
type: 'Dislike'
actor: string actor: string
object: string object: string
} }

View File

@ -1 +1 @@
export type VideoRateType = 'like' | 'dislike' | 'none' export type VideoRateType = 'like' | 'dislike'

Some files were not shown because too many files have changed in this diff Show More