Merge from upstream
This commit is contained in:
commit
ae28cdf327
|
@ -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).
|
||||||
|
|
45
README.md
45
README.md
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 */ }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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`
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'] }"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
@ -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
|
||||||
|
|
|
@ -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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
2163
client/yarn.lock
2163
client/yarn.lock
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
|
@ -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": {
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
npm run spectacle-docs -- -t support/doc/api/html support/doc/api/openapi.yaml
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
server.ts
16
server.ts
|
@ -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 -----------
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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[] = []
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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++
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[] = [
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 () {
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,4 @@
|
||||||
|
import './client'
|
||||||
|
import './fetch'
|
||||||
|
import './helpers'
|
||||||
|
import './security'
|
|
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
|
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
|
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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": []
|
||||||
|
}
|
|
@ -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-----"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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-----"
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -1 +1,2 @@
|
||||||
import './redundancy'
|
import './redundancy'
|
||||||
|
import './activitypub'
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -6,3 +6,4 @@ import './jobs'
|
||||||
import './reverse-proxy'
|
import './reverse-proxy'
|
||||||
import './stats'
|
import './stats'
|
||||||
import './tracker'
|
import './tracker'
|
||||||
|
import './no-client'
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -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 ])
|
||||||
|
|
|
@ -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/'
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
function buildRequestStub (): any {
|
||||||
|
return { }
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResponseStub (): any {
|
||||||
|
return {
|
||||||
|
locals: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildResponseStub,
|
||||||
|
buildRequestStub
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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 () {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export interface DislikeObject {
|
export interface DislikeObject {
|
||||||
type: 'Dislike',
|
id: string
|
||||||
|
type: 'Dislike'
|
||||||
actor: string
|
actor: string
|
||||||
object: string
|
object: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue