Merge from upstream
This commit is contained in:
commit
ae28cdf327
|
@ -2,13 +2,15 @@
|
|||
|
||||
Interested in contributing? Awesome!
|
||||
|
||||
**Quick Links:**
|
||||
**This guide will present you the following contribution topics:**
|
||||
|
||||
* [Translate](#translate)
|
||||
* [Give your feedback](#give-your-feedback)
|
||||
* [Write documentation](#write-documentation)
|
||||
* [Develop](#develop)
|
||||
|
||||
* [Improve the website](#improve-the-website)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
* [Tutorials](#tutorials)
|
||||
|
||||
## Translate
|
||||
|
||||
|
@ -37,6 +39,15 @@ Some hints:
|
|||
* 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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Depending on your OS, you may face the following error :
|
||||
```
|
||||
$ [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
|
||||
### Testing the federation of PeerTube servers
|
||||
|
||||
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):
|
||||
|
@ -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`.
|
||||
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">
|
||||
<a href="https://joinpeertube.org">
|
||||
<img src="https://joinpeertube.org/img/brand.png" alt="PeerTube">
|
||||
<a>
|
||||
<img src="https://joinpeertube.org/img/brand.png" alt="PeerTube">
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<p align=center>
|
||||
<strong><a href="https://joinpeertube.org">Website</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>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
---
|
||||
|
@ -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
|
||||
[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
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
@ -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)).
|
||||
|
||||
: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
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
@ -179,9 +183,8 @@ See [ARCHITECTURE.md](/ARCHITECTURE.md) for a more detailed explanation of the a
|
|||
#### Backend
|
||||
|
||||
* REST API:
|
||||
* Quick Start: [/support/doc/api/quickstart.md](/support/doc/api/quickstart.md)
|
||||
* Swagger/OpenAPI schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml)
|
||||
* 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)
|
||||
* OpenAPI 3.0.0 schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml)
|
||||
* HTML explorer: [docs.joinpeertube.org/api.html](http://docs.joinpeertube.org/api.html)
|
||||
* Servers communicate with each other with [Activity
|
||||
Pub](https://www.w3.org/TR/activitypub/).
|
||||
* Each server has its own users who query it (search videos, query where the
|
||||
|
|
|
@ -63,23 +63,23 @@
|
|||
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.8.3",
|
||||
"@angular/animations": "~6.1.4",
|
||||
"@angular/cli": "~6.2.3",
|
||||
"@angular/common": "~6.1.4",
|
||||
"@angular/compiler": "~6.1.4",
|
||||
"@angular/compiler-cli": "~6.1.4",
|
||||
"@angular/core": "~6.1.4",
|
||||
"@angular/forms": "~6.1.4",
|
||||
"@angular/http": "~6.1.4",
|
||||
"@angular/language-service": "~6.1.4",
|
||||
"@angular/platform-browser": "~6.1.4",
|
||||
"@angular/platform-browser-dynamic": "~6.1.4",
|
||||
"@angular/router": "~6.1.4",
|
||||
"@angular/service-worker": "~6.1.4",
|
||||
"@angular-devkit/build-angular": "~0.10.0",
|
||||
"@angular/animations": "~7.0.2",
|
||||
"@angular/cli": "~7.0.4",
|
||||
"@angular/common": "~7.0.2",
|
||||
"@angular/compiler": "~7.0.2",
|
||||
"@angular/compiler-cli": "~7.0.2",
|
||||
"@angular/core": "~7.0.2",
|
||||
"@angular/forms": "~7.0.2",
|
||||
"@angular/http": "~7.0.2",
|
||||
"@angular/language-service": "~7.0.2",
|
||||
"@angular/platform-browser": "~7.0.2",
|
||||
"@angular/platform-browser-dynamic": "~7.0.2",
|
||||
"@angular/router": "~7.0.2",
|
||||
"@angular/service-worker": "~7.0.2",
|
||||
"@angularclass/hmr": "^2.1.3",
|
||||
"@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/http-client": "^2.2.0",
|
||||
"@ngx-loading-bar/router": "^2.2.0",
|
||||
|
@ -129,7 +129,6 @@
|
|||
"ngx-clipboard": "11.1.7",
|
||||
"ngx-pipes": "^2.1.7",
|
||||
"ngx-qrcode2": "^0.0.9",
|
||||
"ngx-textarea-autosize": "^2.0.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"npm-font-source-sans-pro": "^1.0.2",
|
||||
"path-browserify": "^1.0.0",
|
||||
|
@ -139,17 +138,17 @@
|
|||
"purify-css": "^1.2.5",
|
||||
"purifycss-webpack": "^0.7.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"rxjs": "^6.1.0",
|
||||
"rxjs": "^6.3.3",
|
||||
"sanitize-html": "^1.18.4",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sass-resources-loader": "^1.2.1",
|
||||
"sass-resources-loader": "^2.0.0",
|
||||
"stream-browserify": "^2.0.1",
|
||||
"stream-http": "^2.8.3",
|
||||
"stream-http": "^3.0.0",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"ts-jest": "^23.1.4",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-config-standard": "^8.0.1",
|
||||
"typescript": "2.9",
|
||||
"typescript": "3.1.6",
|
||||
"video.js": "^7",
|
||||
"videojs-contextmenu-ui": "^5.0.0",
|
||||
"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'
|
||||
|
||||
export abstract class UserEdit extends FormReactive {
|
||||
|
||||
videoQuotaOptions: { 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] }))
|
||||
username: string
|
||||
|
||||
protected abstract serverService: ServerService
|
||||
protected abstract configService: ConfigService
|
||||
|
|
|
@ -86,4 +86,4 @@
|
|||
</ng-template>
|
||||
</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)
|
||||
}
|
||||
|
||||
onUsersBanned () {
|
||||
onUserChanged () {
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,11 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
|||
export abstract class MyAccountVideoChannelEdit extends FormReactive {
|
||||
// We need it even in the create component because it's used in the edit template
|
||||
videoChannelToUpdate: VideoChannel
|
||||
instanceHost: string
|
||||
|
||||
abstract isCreation (): boolean
|
||||
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 {
|
||||
titlePage: string
|
||||
marginContent = false // Disable margin
|
||||
currentRoute = '/video-channel/videos'
|
||||
currentRoute = '/video-channels/videos'
|
||||
loadOnInit = false
|
||||
|
||||
private videoChannel: VideoChannel
|
||||
|
@ -55,7 +55,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
|
|||
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
|
||||
.subscribe(videoChannel => {
|
||||
this.videoChannel = videoChannel
|
||||
this.currentRoute = '/video-channel/' + this.videoChannel.uuid + '/videos'
|
||||
this.currentRoute = '/video-channels/' + this.videoChannel.uuid + '/videos'
|
||||
|
||||
this.reloadVideos()
|
||||
this.generateSyndicationList()
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
.icon.icon-upload {
|
||||
@include icon(22px);
|
||||
|
||||
background-image: url('../../assets/images/header/upload.svg');
|
||||
background-image: url('../../assets/images/header/upload-white.svg');
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
|
|
|
@ -131,10 +131,14 @@ menu {
|
|||
transition: background-color .1s ease-in-out;
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
&:hover, &.focus-visible {
|
||||
&.active {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&:hover, &.focus-visible {
|
||||
background-color: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include icon(22px);
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './form-validators'
|
||||
export * from './form-reactive'
|
||||
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) { }
|
||||
|
||||
transform (value: number) {
|
||||
const seconds = Math.floor((Date.now() - value) / 1000)
|
||||
transform (arg: number | Date | string) {
|
||||
const argDate = new Date(arg)
|
||||
const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
|
||||
|
||||
let interval = Math.floor(seconds / 31536000)
|
||||
if (interval > 1) {
|
||||
|
|
|
@ -37,13 +37,15 @@ import {
|
|||
LoginValidatorsService,
|
||||
ReactiveFileComponent,
|
||||
ResetPasswordValidatorsService,
|
||||
TextareaAutoResizeDirective,
|
||||
UserValidatorsService,
|
||||
VideoAbuseValidatorsService,
|
||||
VideoAcceptOwnershipValidatorsService,
|
||||
VideoBlacklistValidatorsService,
|
||||
VideoChangeOwnershipValidatorsService,
|
||||
VideoChannelValidatorsService,
|
||||
VideoCommentValidatorsService,
|
||||
VideoValidatorsService,
|
||||
VideoChangeOwnershipValidatorsService, VideoAcceptOwnershipValidatorsService
|
||||
VideoValidatorsService
|
||||
} from '@app/shared/forms'
|
||||
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
|
||||
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 { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
|
||||
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 { OverviewService } from '@app/shared/overview'
|
||||
import { UserBanModalComponent } from '@app/shared/moderation'
|
||||
|
@ -92,6 +94,7 @@ import { BlocklistService } from '@app/shared/blocklist'
|
|||
FromNowPipe,
|
||||
MarkdownTextareaComponent,
|
||||
InfiniteScrollerDirective,
|
||||
TextareaAutoResizeDirective,
|
||||
HelpComponent,
|
||||
ReactiveFileComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
|
@ -129,6 +132,7 @@ import { BlocklistService } from '@app/shared/blocklist'
|
|||
ActionDropdownComponent,
|
||||
MarkdownTextareaComponent,
|
||||
InfiniteScrollerDirective,
|
||||
TextareaAutoResizeDirective,
|
||||
HelpComponent,
|
||||
ReactiveFileComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
class="video-miniature-name"
|
||||
[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="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
|
||||
|
||||
{{ video.name }}
|
||||
</a>
|
||||
|
||||
<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 {
|
||||
UserVideoRate,
|
||||
UserVideoRateType,
|
||||
UserVideoRateUpdate,
|
||||
VideoConstant,
|
||||
VideoFilter,
|
||||
VideoPrivacy,
|
||||
VideoRateType,
|
||||
VideoUpdate
|
||||
} from '../../../../../shared/models/videos'
|
||||
import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
|
||||
|
@ -332,7 +332,7 @@ export class VideoService implements VideosProvider {
|
|||
return privacies
|
||||
}
|
||||
|
||||
private setVideoRate (id: number, rateType: VideoRateType) {
|
||||
private setVideoRate (id: number, rateType: UserVideoRateType) {
|
||||
const url = VideoService.BASE_VIDEO_URL + id + '/rate'
|
||||
const body: UserVideoRateUpdate = {
|
||||
rating: rateType
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<img [src]="getAvatarUrl()" alt="Avatar" />
|
||||
|
||||
<div class="form-group">
|
||||
<textarea i18n-placeholder placeholder="Add comment..." autosize
|
||||
<textarea i18n-placeholder placeholder="Add comment..." myAutoResize
|
||||
[readonly]="(user === null) ? true : false"
|
||||
(click)="openVisitorModal($event)"
|
||||
formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
|
||||
|
|
|
@ -29,9 +29,9 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
|
|||
@Output() commentCreated = new EventEmitter<VideoCommentCreate>()
|
||||
|
||||
@ViewChild('visitorModal') visitorModal: NgbModal
|
||||
@ViewChild('textarea') private textareaElement: ElementRef
|
||||
@ViewChild('textarea') textareaElement: ElementRef
|
||||
|
||||
private addingComment = false
|
||||
addingComment = false
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
|
|
|
@ -450,7 +450,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.checkUserRating()
|
||||
}
|
||||
|
||||
private setRating (nextRating: VideoRateType) {
|
||||
private setRating (nextRating: UserVideoRateType) {
|
||||
let method
|
||||
switch (nextRating) {
|
||||
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 dislikesToIncrement = 0
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import { NgxQRCodeModule } from 'ngx-qrcode2'
|
|||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
|
||||
import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
|
||||
import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -26,7 +25,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
|
|||
ClipboardModule,
|
||||
NgbTooltipModule,
|
||||
NgxQRCodeModule,
|
||||
TextareaAutosizeModule,
|
||||
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
|
||||
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`.
|
||||
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
display: block;
|
||||
/* Fallback for non-webkit */
|
||||
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 */
|
||||
font-size: $font-size;
|
||||
line-height: $line-height;
|
||||
|
@ -511,4 +511,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,9 +256,8 @@ class PeerTubeEmbed {
|
|||
}
|
||||
|
||||
private async initCore () {
|
||||
const urlParts = window.location.href.split('/')
|
||||
const lastPart = urlParts[ urlParts.length - 1 ]
|
||||
const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[ 0 ]
|
||||
const urlParts = window.location.pathname.split('/')
|
||||
const videoId = urlParts[ urlParts.length - 1 ]
|
||||
|
||||
const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
|
||||
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:client": "scripty",
|
||||
"start": "node dist/server",
|
||||
"start:server": "node dist/server --no-client",
|
||||
"update-host": "node ./dist/scripts/update-host.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",
|
||||
"test": "scripty",
|
||||
"help": "scripty",
|
||||
"generate-api-doc": "scripty",
|
||||
"generate-cli-doc": "scripty",
|
||||
"parse-log": "node ./dist/scripts/parse-log.js",
|
||||
"prune-storage": "node ./dist/scripts/prune-storage.js",
|
||||
"optimize-old-videos": "node ./dist/scripts/optimize-old-videos.js",
|
||||
"postinstall": "cd client && yarn install --pure-lockfile",
|
||||
"tsc": "tsc",
|
||||
"spectacle-docs": "node_modules/spectacle-docs/bin/spectacle.js",
|
||||
"commander": "commander",
|
||||
"ng": "ng",
|
||||
"nodemon": "nodemon",
|
||||
|
@ -71,13 +70,21 @@
|
|||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
"pre-commit": "./scripts/openapi-peertube-version.sh && lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.scss": [
|
||||
"sass-lint -c client/.sass-lint.yml",
|
||||
"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": {
|
||||
|
@ -88,7 +95,7 @@
|
|||
"async": "^2.0.0",
|
||||
"async-lock": "^1.1.2",
|
||||
"async-lru": "^1.1.1",
|
||||
"bcrypt": "2",
|
||||
"bcrypt": "3.0.2",
|
||||
"bittorrent-tracker": "^9.0.0",
|
||||
"bluebird": "^3.5.0",
|
||||
"body-parser": "^1.12.4",
|
||||
|
@ -113,7 +120,7 @@
|
|||
"http-signature": "^1.2.0",
|
||||
"ip-anonymize": "^0.0.6",
|
||||
"ipaddr.js": "1.8.1",
|
||||
"is-cidr": "^2.0.5",
|
||||
"is-cidr": "^3.0.0",
|
||||
"iso-639-3": "^1.0.1",
|
||||
"js-yaml": "^3.5.4",
|
||||
"jsonld": "^1.0.1",
|
||||
|
@ -138,9 +145,9 @@
|
|||
"request": "^2.81.0",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"scripty": "^1.5.0",
|
||||
"sequelize": "4.38.0",
|
||||
"sequelize": "4.41.2",
|
||||
"sequelize-typescript": "0.6.6",
|
||||
"sharp": "^0.20.0",
|
||||
"sharp": "^0.21.0",
|
||||
"srt-to-vtt": "^1.1.2",
|
||||
"summon-install": "^0.4.3",
|
||||
"useragent": "^2.3.0",
|
||||
|
@ -155,7 +162,7 @@
|
|||
"devDependencies": {
|
||||
"@types/async": "^2.0.40",
|
||||
"@types/async-lock": "^1.1.0",
|
||||
"@types/bcrypt": "^2.0.0",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/bluebird": "3.5.21",
|
||||
"@types/body-parser": "^1.16.3",
|
||||
"@types/bull": "^3.3.12",
|
||||
|
@ -183,7 +190,7 @@
|
|||
"@types/pem": "^1.9.3",
|
||||
"@types/redis": "^2.8.5",
|
||||
"@types/request": "^2.0.3",
|
||||
"@types/sharp": "^0.17.6",
|
||||
"@types/sharp": "^0.21.0",
|
||||
"@types/supertest": "^2.0.3",
|
||||
"@types/validator": "^9.4.0",
|
||||
"@types/webtorrent": "^0.98.4",
|
||||
|
@ -192,19 +199,19 @@
|
|||
"chai-json-schema": "^1.5.0",
|
||||
"chai-xml": "^0.3.2",
|
||||
"husky": "^1.0.0-rc.4",
|
||||
"libxmljs": "0.19.3",
|
||||
"lint-staged": "^7.1.0",
|
||||
"libxmljs": "0.19.5",
|
||||
"lint-staged": "^8.0.4",
|
||||
"maildev": "^1.0.0-rc3",
|
||||
"mocha": "^5.0.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"sass-lint": "^1.12.1",
|
||||
"source-map-support": "^0.5.0",
|
||||
"spectacle-docs": "^1.0.2",
|
||||
"supertest": "^3.0.0",
|
||||
"swagger-cli": "^2.2.0",
|
||||
"ts-node": "7.0.1",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-config-standard": "^8.0.1",
|
||||
"typescript": "^2.5.2",
|
||||
"typescript": "^3.1.6",
|
||||
"xliff": "^4.0.0"
|
||||
},
|
||||
"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
|
||||
npm run build -- --light-fr
|
||||
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/misc-endpoints.ts \
|
||||
server/tests/helpers/index.ts
|
||||
|
@ -31,7 +30,7 @@ elif [ "$1" = "api-2" ]; then
|
|||
elif [ "$1" = "api-3" ]; then
|
||||
npm run build:server
|
||||
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
|
||||
mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-4.ts
|
||||
elif [ "$1" = "lint" ]; then
|
||||
|
|
|
@ -4,7 +4,7 @@ import { VideoModel } from '../server/models/video/video'
|
|||
import { ActorModel } from '../server/models/activitypub/actor'
|
||||
import {
|
||||
getAccountActivityPubUrl,
|
||||
getAnnounceActivityPubUrl,
|
||||
getVideoAnnounceActivityPubUrl,
|
||||
getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
|
||||
getVideoCommentActivityPubUrl
|
||||
} from '../server/lib/activitypub'
|
||||
|
@ -78,7 +78,7 @@ async function run () {
|
|||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
16
server.ts
16
server.ts
|
@ -16,6 +16,7 @@ import * as cookieParser from 'cookie-parser'
|
|||
import * as helmet from 'helmet'
|
||||
import * as useragent from 'useragent'
|
||||
import * as anonymize from 'ip-anonymize'
|
||||
import * as cli from 'commander'
|
||||
|
||||
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)
|
||||
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()
|
||||
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 { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
|
||||
import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
|
||||
import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
|
||||
|
||||
// ----------- Command line -----------
|
||||
|
||||
cli
|
||||
.option('--no-client', 'Start PeerTube without client interface')
|
||||
.parse(process.argv)
|
||||
|
||||
// ----------- App -----------
|
||||
|
||||
// Enable CORS for develop
|
||||
|
@ -126,7 +132,11 @@ app.use(morgan('combined', {
|
|||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(bodyParser.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
|
||||
app.use(cookieParser())
|
||||
|
@ -151,7 +161,7 @@ app.use('/', trackerRouter)
|
|||
app.use('/', staticRouter)
|
||||
|
||||
// Client files, last valid routes!
|
||||
app.use('/', clientsRouter)
|
||||
if (cli.client) app.use('/', clientsRouter)
|
||||
|
||||
// ----------- Errors -----------
|
||||
|
||||
|
|
|
@ -3,17 +3,22 @@ import * as express from 'express'
|
|||
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
|
||||
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
|
||||
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 { buildCreateActivity } from '../../lib/activitypub/send/send-create'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
videosShareValidator,
|
||||
executeIfActivityPub,
|
||||
localAccountValidator,
|
||||
localVideoChannelValidator,
|
||||
videosCustomGetValidator
|
||||
} from '../../middlewares'
|
||||
import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
|
||||
import {
|
||||
getAccountVideoRateValidator,
|
||||
videoCommentGetValidator,
|
||||
videosGetValidator
|
||||
} from '../../middlewares/validators'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { ActorModel } from '../../models/activitypub/actor'
|
||||
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||
|
@ -25,6 +30,7 @@ import { cacheRoute } from '../../middlewares/cache'
|
|||
import { activityPubResponse } from './utils'
|
||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import {
|
||||
getRateUrl,
|
||||
getVideoCommentsActivityPubUrl,
|
||||
getVideoDislikesActivityPubUrl,
|
||||
getVideoLikesActivityPubUrl,
|
||||
|
@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
|
|||
executeIfActivityPub(asyncMiddleware(localAccountValidator)),
|
||||
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',
|
||||
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(videoAnnouncesController))
|
||||
)
|
||||
activityPubClientRouter.get('/videos/watch/:id/announces/:accountId',
|
||||
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
||||
executeIfActivityPub(asyncMiddleware(videosShareValidator)),
|
||||
executeIfActivityPub(asyncMiddleware(videoAnnounceController))
|
||||
)
|
||||
|
@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
|
|||
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) {
|
||||
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)
|
||||
return {
|
||||
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)
|
||||
|
|
|
@ -43,11 +43,13 @@ export {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
function inboxController (req: express.Request, res: express.Response) {
|
||||
const rootActivity: RootActivity = req.body
|
||||
let activities: Activity[] = []
|
||||
|
||||
|
|
|
@ -405,7 +405,11 @@ async function viewVideo (req: express.Request, res: express.Response) {
|
|||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import * as express from 'express'
|
|||
import { UserVideoRateUpdate } from '../../../../shared'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
|
||||
import { sendVideoRateChange } from '../../../lib/activitypub'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares'
|
||||
import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
|
||||
import { AccountModel } from '../../../models/account/account'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
|
@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
|
|||
|
||||
rateVideoRouter.put('/:id/rate',
|
||||
authenticate,
|
||||
asyncMiddleware(videoRateValidator),
|
||||
asyncMiddleware(videoUpdateRateValidator),
|
||||
asyncRetryTransactionMiddleware(rateVideo)
|
||||
)
|
||||
|
||||
|
@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
|
|||
const body: UserVideoRateUpdate = req.body
|
||||
const rateType = body.rating
|
||||
const videoInstance: VideoModel = res.locals.video
|
||||
const userAccount: AccountModel = res.locals.oauth.token.User.Account
|
||||
|
||||
await sequelizeTypescript.transaction(async 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)
|
||||
|
||||
let likesToIncrement = 0
|
||||
|
@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
|
|||
// There was a previous rate, update it
|
||||
if (previousRate) {
|
||||
// We will remove the previous rate, so we will need to update the video count attribute
|
||||
if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement--
|
||||
else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
|
||||
if (previousRate.type === 'like') likesToIncrement--
|
||||
else if (previousRate.type === 'dislike') dislikesToIncrement--
|
||||
|
||||
if (rateType === 'none') { // Destroy previous rate
|
||||
await previousRate.destroy(sequelizeOptions)
|
||||
} else { // Update previous rate
|
||||
previousRate.type = rateType
|
||||
previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
|
||||
await previousRate.save(sequelizeOptions)
|
||||
}
|
||||
} else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
|
||||
const query = {
|
||||
accountId: accountInstance.id,
|
||||
videoId: videoInstance.id,
|
||||
type: rateType
|
||||
type: rateType,
|
||||
url: getRateUrl(rateType, userAccount.Actor, videoInstance)
|
||||
}
|
||||
|
||||
await AccountVideoRateModel.create(query, sequelizeOptions)
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ACTIVITY_PUB } from '../initializers'
|
|||
import { ActorModel } from '../models/activitypub/actor'
|
||||
import { signJsonLDObject } from './peertube-crypto'
|
||||
import { pageToStartAndCount } from './core-utils'
|
||||
import { parse } from 'url'
|
||||
|
||||
function activityPubContextify <T> (data: T) {
|
||||
return Object.assign(data, {
|
||||
|
@ -24,7 +25,7 @@ function activityPubContextify <T> (data: T) {
|
|||
sensitive: 'as:sensitive',
|
||||
language: 'sc:inLanguage',
|
||||
views: 'sc:Number',
|
||||
stats: 'sc:Number',
|
||||
state: 'sc:Number',
|
||||
size: 'sc:Number',
|
||||
fps: 'sc:Number',
|
||||
commentsEnabled: 'sc:Boolean',
|
||||
|
@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
|
|||
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 {
|
||||
checkUrlsSameHost,
|
||||
getActorUrl,
|
||||
activityPubContextify,
|
||||
activityPubCollectionPagination,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as AsyncLRU from 'async-lru'
|
||||
import * as jsonld from 'jsonld/'
|
||||
import * as jsonld from 'jsonld'
|
||||
import * as jsig from 'jsonld-signatures'
|
||||
|
||||
const nodeDocumentLoader = jsonld.documentLoaders.node()
|
||||
|
@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
|
|||
|
||||
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('-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('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
|
||||
.outputOption('-map_metadata -1') // strip all metadata
|
||||
.outputOption('-movflags faststart')
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { Request } from 'express'
|
||||
import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
|
||||
import { ActorModel } from '../models/activitypub/actor'
|
||||
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils'
|
||||
import { jsig } from './custom-jsonld-signature'
|
||||
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
|
||||
import { jsig, jsonld } from './custom-jsonld-signature'
|
||||
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')
|
||||
|
||||
|
@ -30,21 +33,36 @@ async function cryptPassword (password: string) {
|
|||
|
||||
// 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
|
||||
}
|
||||
|
||||
function parseHTTPSignature (req: Request) {
|
||||
return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME })
|
||||
function parseHTTPSignature (req: Request, clockSkew?: number) {
|
||||
return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
'@context': jsig.SECURITY_CONTEXT_URL,
|
||||
id: fromActor.url,
|
||||
type: 'CryptographicKey',
|
||||
type: 'CryptographicKey',
|
||||
owner: fromActor.url,
|
||||
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) {
|
||||
const options = {
|
||||
privateKeyPem: byActor.privateKey,
|
||||
|
@ -82,6 +138,7 @@ function signJsonLDObject (byActor: ActorModel, data: any) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isHTTPSignatureDigestValid,
|
||||
parseHTTPSignature,
|
||||
isHTTPSignatureVerified,
|
||||
isJsonLDSignatureVerified,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra'
|
|||
import * as request from 'request'
|
||||
import { ACTIVITY_PUB } from '../initializers'
|
||||
|
||||
function doRequest (
|
||||
function doRequest <T> (
|
||||
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
|
||||
): Bluebird<{ response: request.RequestResponse, body: any }> {
|
||||
if (requestOptions.activityPub === true) {
|
||||
|
@ -11,7 +11,7 @@ function doRequest (
|
|||
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 }))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
URL: { min: 3, max: 2000 } // Length
|
||||
},
|
||||
VIDEO_RATES: {
|
||||
URL: { min: 3, max: 2000 } // Length
|
||||
},
|
||||
VIDEOS: {
|
||||
NAME: { min: 3, max: 120 }, // Length
|
||||
LANGUAGE: { min: 1, max: 10 }, // Length
|
||||
|
@ -535,7 +538,7 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
|||
const HTTP_SIGNATURE = {
|
||||
HEADER_NAME: 'signature',
|
||||
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 { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
|
||||
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 { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
|
||||
|
@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
|
|||
const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
|
||||
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 {
|
||||
// Assert we don't recurse another time
|
||||
// Don't recurse another time
|
||||
ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
|
||||
} catch (err) {
|
||||
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)
|
||||
|
||||
const actorJSON: ActivityPubActor = requestResult.body
|
||||
|
||||
if (isActorObjectValid(actorJSON) === false) {
|
||||
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
|
||||
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 followingCount = await fetchActorTotalItems(actorJSON.following)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
|
|||
import { doRequest } from '../../helpers/requests'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
|
||||
|
||||
async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
|
||||
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
|
||||
}
|
||||
|
||||
const response = await doRequest(options)
|
||||
const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
|
||||
const firstBody = response.body
|
||||
|
||||
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) {
|
||||
options.uri = nextLink
|
||||
|
||||
const { body } = await doRequest(options)
|
||||
const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
|
||||
nextLink = body.next
|
||||
i++
|
||||
|
||||
|
|
|
@ -1,9 +1 @@
|
|||
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 { Redis } from '../../redis'
|
||||
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) {
|
||||
const activityObject = activity.object
|
||||
|
@ -65,9 +68,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
|
|||
videoId: video.id,
|
||||
accountId: byAccount.id
|
||||
}
|
||||
|
||||
const [ , created ] = await AccountVideoRateModel.findOrCreate({
|
||||
where: rate,
|
||||
defaults: rate,
|
||||
defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
|
||||
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) {
|
||||
const view = activity.object as ViewObject
|
||||
|
||||
const options = {
|
||||
videoObject: view.object,
|
||||
fetchType: 'only-video' as 'only-video'
|
||||
}
|
||||
const { video } = await getOrCreateVideoAndAccountAndChannel(options)
|
||||
const video = await VideoModel.loadByUrl(view.object)
|
||||
if (!video || video.isOwned() === false) return
|
||||
|
||||
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) {
|
||||
|
|
|
@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
|
|||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { immutableAssign } from '../../../tests/utils'
|
||||
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
|
||||
|
||||
async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
|
||||
return retryTransactionWrapper(processLikeVideo, byActor, activity)
|
||||
|
@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
|
|||
}
|
||||
const [ , created ] = await AccountVideoRateModel.findOrCreate({
|
||||
where: rate,
|
||||
defaults: rate,
|
||||
defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
|
||||
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 => {
|
||||
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}.`)
|
||||
|
||||
await rate.destroy({ transaction: t })
|
||||
|
@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
|
|||
return sequelizeTypescript.transaction(async t => {
|
||||
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}.`)
|
||||
|
||||
await rate.destroy({ transaction: t })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Activity, ActivityType } from '../../../../shared/models/activitypub'
|
||||
import { getActorUrl } from '../../../helpers/activitypub'
|
||||
import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { processAcceptActivity } from './process-accept'
|
||||
|
@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
|
|||
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 } = {}
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
|
|||
const actorUrl = getActorUrl(activity.actor)
|
||||
|
||||
// When we fetch remote data, we don't have signature
|
||||
if (signatureActor && actorUrl !== signatureActor.url) {
|
||||
logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url)
|
||||
if (options.signatureActor && actorUrl !== options.signatureActor.url) {
|
||||
logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
|
||||
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
|
||||
|
||||
const activityProcessor = processActivity[activity.type]
|
||||
|
@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
|
|||
}
|
||||
|
||||
try {
|
||||
await activityProcessor(activity, byActor, inboxActor)
|
||||
await activityProcessor(activity, byActor, options.inboxActor)
|
||||
} catch (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)
|
||||
|
||||
const url = getVideoViewActivityPubUrl(byActor, video)
|
||||
const viewActivity = buildViewActivity(byActor, video)
|
||||
const viewActivity = buildViewActivity(url, byActor, video)
|
||||
|
||||
return sendVideoRelatedCreateActivity({
|
||||
// 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)
|
||||
|
||||
const url = getVideoDislikeActivityPubUrl(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(url, byActor, video)
|
||||
|
||||
return sendVideoRelatedCreateActivity({
|
||||
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 {
|
||||
id: url,
|
||||
type: 'Dislike',
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
}
|
||||
}
|
||||
|
||||
function buildViewActivity (byActor: ActorModel, video: VideoModel) {
|
||||
function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
|
||||
return {
|
||||
id: url,
|
||||
type: 'View',
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
|
|
|
@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel,
|
|||
|
||||
return audiencify(
|
||||
{
|
||||
type: 'Like' as 'Like',
|
||||
id: url,
|
||||
type: 'Like' as 'Like',
|
||||
actor: byActor.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)
|
||||
|
||||
const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
|
||||
const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
|
||||
|
||||
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 { VideoShareModel } from '../../models/video/video-share'
|
||||
import { sendUndoAnnounce, sendVideoAnnounce } from './send'
|
||||
import { getAnnounceActivityPubUrl } from './url'
|
||||
import { getVideoAnnounceActivityPubUrl } from './url'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { doRequest } from '../../helpers/requests'
|
||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
|
||||
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
|
||||
|
||||
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
|
||||
if (video.privacy === VideoPrivacy.PRIVATE) return undefined
|
||||
|
@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
|
|||
json: 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 entry = {
|
||||
|
@ -72,7 +77,7 @@ export {
|
|||
async function shareByServer (video: VideoModel, t: Transaction) {
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor)
|
||||
const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
|
||||
return VideoShareModel.findOrCreate({
|
||||
defaults: {
|
||||
actorId: serverActor.id,
|
||||
|
@ -91,7 +96,7 @@ async function shareByServer (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({
|
||||
defaults: {
|
||||
actorId: video.VideoChannel.actorId,
|
||||
|
|
|
@ -33,14 +33,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
|
||||
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
|
||||
return byActor.url + '/dislikes/' + video.id
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
|
|||
return follower.url + '/accepts/follows/' + me.id
|
||||
}
|
||||
|
||||
function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) {
|
||||
return originalUrl + '/announces/' + byActor.id
|
||||
function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
|
||||
return video.url + '/announces/' + byActor.id
|
||||
}
|
||||
|
||||
function getDeleteActivityPubUrl (originalUrl: string) {
|
||||
|
@ -97,7 +97,7 @@ export {
|
|||
getVideoAbuseActivityPubUrl,
|
||||
getActorFollowActivityPubUrl,
|
||||
getActorFollowAcceptActivityPubUrl,
|
||||
getAnnounceActivityPubUrl,
|
||||
getVideoAnnounceActivityPubUrl,
|
||||
getUpdateActivityPubUrl,
|
||||
getUndoActivityPubUrl,
|
||||
getVideoViewActivityPubUrl,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment'
|
|||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from './videos'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { checkUrlsSameHost } from '../../helpers/activitypub'
|
||||
|
||||
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
|
||||
let originCommentId: number = null
|
||||
|
@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
|
|||
const actorUrl = body.attributedTo
|
||||
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 entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
|
||||
if (!entry) return { created: false }
|
||||
|
@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
|
|||
const actorUrl = body.attributedTo
|
||||
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 comment = new VideoCommentModel({
|
||||
url: body.id,
|
||||
|
|
|
@ -8,13 +8,35 @@ import { getOrCreateActorAndServerAndModel } from './actor'
|
|||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import { logger } from '../../helpers/logger'
|
||||
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
|
||||
|
||||
await Bluebird.map(actorUrls, async actorUrl => {
|
||||
await Bluebird.map(ratesUrl, async rateUrl => {
|
||||
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 [ , created ] = await AccountVideoRateModel
|
||||
.findOrCreate({
|
||||
where: {
|
||||
|
@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
|
|||
defaults: {
|
||||
videoId: video.id,
|
||||
accountId: actor.Account.id,
|
||||
type: rate
|
||||
type: rate,
|
||||
url: body.id
|
||||
}
|
||||
})
|
||||
|
||||
if (created) rateCounts += 1
|
||||
} 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 })
|
||||
|
||||
|
@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
|
|||
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 {
|
||||
getRateUrl,
|
||||
createRates,
|
||||
sendVideoRateChange
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { createRates } from './video-rates'
|
|||
import { addVideoShares, shareVideoByServerAndChannel } from './share'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
|
||||
import { checkUrlsSameHost } from '../../helpers/activitypub'
|
||||
|
||||
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
|
||||
// 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)
|
||||
|
||||
if (sanitizeAndCheckVideoTorrentObject(body) === false) {
|
||||
if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
|
||||
logger.debug('Remote video JSON is not valid.', { body })
|
||||
return { response, videoObject: undefined }
|
||||
}
|
||||
|
@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
|
|||
const channel = videoObject.attributedTo.find(a => a.type === 'Group')
|
||||
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')
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
|
|||
if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
|
||||
|
||||
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-dislikes': items => createRates(items, video, 'dislike'),
|
||||
'video-shares': items => addVideoShares(items, video),
|
||||
|
|
|
@ -38,15 +38,20 @@ async function buildSignedRequestOptions (payload: Payload) {
|
|||
}
|
||||
}
|
||||
|
||||
function buildGlobalHeaders (body: object) {
|
||||
const digest = 'SHA-256=' + sha256(JSON.stringify(body), 'base64')
|
||||
|
||||
function buildGlobalHeaders (body: any) {
|
||||
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 {
|
||||
buildDigest,
|
||||
buildGlobalHeaders,
|
||||
computeBody,
|
||||
buildSignedRequestOptions
|
||||
|
|
|
@ -3,8 +3,9 @@ import { logger } from '../../../helpers/logger'
|
|||
import { VideoModel } from '../../../models/video/video'
|
||||
import { VideoViewModel } from '../../../models/video/video-views'
|
||||
import { isTestInstance } from '../../../helpers/core-utils'
|
||||
import { federateVideoIfNeeded } from '../../activitypub'
|
||||
|
||||
async function processVideosViewsViews () {
|
||||
async function processVideosViews () {
|
||||
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
|
||||
|
@ -36,6 +37,9 @@ async function processVideosViewsViews () {
|
|||
views,
|
||||
videoId
|
||||
})
|
||||
|
||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
|
||||
if (video.isOwned()) await federateVideoIfNeeded(video, false)
|
||||
} catch (err) {
|
||||
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 {
|
||||
processVideosViewsViews
|
||||
processVideosViews
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { EmailPayload, processEmail } from './handlers/email'
|
|||
import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file'
|
||||
import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
|
||||
import { processVideoImport, VideoImportPayload } from './handlers/video-import'
|
||||
import { processVideosViewsViews } from './handlers/video-views'
|
||||
import { processVideosViews } from './handlers/video-views'
|
||||
|
||||
type CreateJobArgument =
|
||||
{ type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
|
||||
|
@ -32,7 +32,7 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
|
|||
'video-file': processVideoFile,
|
||||
'email': processEmail,
|
||||
'video-import': processVideoImport,
|
||||
'videos-views': processVideosViewsViews
|
||||
'videos-views': processVideosViews
|
||||
}
|
||||
|
||||
const jobTypes: JobType[] = [
|
||||
|
|
|
@ -185,11 +185,12 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
|
|||
}
|
||||
|
||||
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 totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(filesToDuplicate)
|
||||
|
||||
return totalDuplicated > maxSize
|
||||
return totalWillDuplicate > maxSize
|
||||
}
|
||||
|
||||
private buildNewExpiration (expiresAfterMs: number) {
|
||||
|
|
|
@ -53,7 +53,8 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
|
|||
|
||||
export {
|
||||
checkSignature,
|
||||
executeIfActivityPub
|
||||
executeIfActivityPub,
|
||||
checkHttpSignature
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -94,7 +95,7 @@ async function checkHttpSignature (req: Request, res: Response) {
|
|||
async function checkJsonLDSignature (req: Request, res: Response) {
|
||||
const signatureObject: ActivityPubSignature = req.body.signature
|
||||
|
||||
if (!signatureObject.creator) {
|
||||
if (!signatureObject || !signatureObject.creator) {
|
||||
res.sendStatus(403)
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -5,4 +5,6 @@ export * from './video-channels'
|
|||
export * from './video-comments'
|
||||
export * from './video-imports'
|
||||
export * from './video-watch'
|
||||
export * from './video-rates'
|
||||
export * from './video-shares'
|
||||
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,
|
||||
isVideoNameValid,
|
||||
isVideoPrivacyValid,
|
||||
isVideoRatingTypeValid,
|
||||
isVideoSupportValid,
|
||||
isVideoTagsValid
|
||||
} from '../../../helpers/custom-validators/videos'
|
||||
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||
import { VideoShareModel } from '../../../models/video/video-share'
|
||||
import { authenticate } from '../../oauth'
|
||||
import { areValidationErrors } from '../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 = [
|
||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
|
@ -415,9 +378,6 @@ export {
|
|||
videosGetValidator,
|
||||
videosCustomGetValidator,
|
||||
videosRemoveValidator,
|
||||
videosShareValidator,
|
||||
|
||||
videoRateValidator,
|
||||
|
||||
videosChangeOwnershipValidator,
|
||||
videosTerminateChangeOwnershipValidator,
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { values } from 'lodash'
|
||||
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 { 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 { AccountModel } from './account'
|
||||
import { ActorModel } from '../activitypub/actor'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
|
||||
/*
|
||||
Account rates per video.
|
||||
|
@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
|
|||
},
|
||||
{
|
||||
fields: [ 'videoId', 'type' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'url' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
@Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
|
||||
type: VideoRateType
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
|
||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
|
||||
url: string
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
|
@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
})
|
||||
Account: AccountModel
|
||||
|
||||
static load (accountId: number, videoId: number, transaction: Transaction) {
|
||||
static load (accountId: number, videoId: number, transaction?: Transaction) {
|
||||
const options: IFindOptions<AccountVideoRateModel> = {
|
||||
where: {
|
||||
accountId,
|
||||
|
@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
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) {
|
||||
const query = {
|
||||
offset: start,
|
||||
|
|
|
@ -47,7 +47,7 @@ enum ScopeNames {
|
|||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
attributes: [ 'id', 'url' ],
|
||||
model: () => ActorModel.unscoped(),
|
||||
required: true
|
||||
}
|
||||
|
|
|
@ -293,6 +293,11 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
|||
}
|
||||
|
||||
return VideoFileModel.sum('size', options as any) // FIXME: typings
|
||||
.then(v => {
|
||||
if (!v || isNaN(v)) return 0
|
||||
|
||||
return v
|
||||
})
|
||||
}
|
||||
|
||||
static async listLocalExpired () {
|
||||
|
|
|
@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
|
|||
})
|
||||
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({
|
||||
where: {
|
||||
actorId,
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
setAccessTokensToServers
|
||||
} from '../../shared/utils'
|
||||
|
||||
|
||||
const expect = chai.expect
|
||||
|
||||
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 './activitypub'
|
||||
|
|
|
@ -54,7 +54,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams:
|
|||
immutableAssign({
|
||||
min_lifetime: '1 hour',
|
||||
strategy: strategy,
|
||||
size: '100KB'
|
||||
size: '200KB'
|
||||
}, additionalParams)
|
||||
]
|
||||
}
|
||||
|
@ -111,8 +111,8 @@ async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
|
|||
const stat = data.videosRedundancy[0]
|
||||
|
||||
expect(stat.strategy).to.equal(strategy)
|
||||
expect(stat.totalSize).to.equal(102400)
|
||||
expect(stat.totalUsed).to.be.at.least(1).and.below(102401)
|
||||
expect(stat.totalSize).to.equal(204800)
|
||||
expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
|
||||
expect(stat.totalVideoFiles).to.equal(4)
|
||||
expect(stat.totalVideos).to.equal(1)
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
|
|||
|
||||
const stat = data.videosRedundancy[0]
|
||||
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.totalVideoFiles).to.equal(0)
|
||||
expect(stat.totalVideos).to.equal(0)
|
||||
|
@ -223,7 +223,7 @@ describe('Test videos redundancy', function () {
|
|||
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)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
@ -270,7 +270,7 @@ describe('Test videos redundancy', function () {
|
|||
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)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
@ -338,7 +338,7 @@ describe('Test videos redundancy', function () {
|
|||
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)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
@ -419,7 +419,7 @@ describe('Test videos redundancy', function () {
|
|||
|
||||
killallServers([ servers[0] ])
|
||||
|
||||
await wait(10000)
|
||||
await wait(15000)
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
it('Should cache video 2 webseed on the first video', async function () {
|
||||
this.timeout(50000)
|
||||
it('Should cache video 2 webseeds on the first video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await wait(7000)
|
||||
let checked = false
|
||||
|
||||
try {
|
||||
await check1WebSeed(strategy, video1Server2UUID)
|
||||
await check2Webseeds(strategy, video2Server2UUID)
|
||||
} catch {
|
||||
await wait(3000)
|
||||
while (checked === false) {
|
||||
await wait(1000)
|
||||
|
||||
try {
|
||||
await check1WebSeed(strategy, video1Server2UUID)
|
||||
await check2Webseeds(strategy, video2Server2UUID)
|
||||
} catch {
|
||||
await wait(5000)
|
||||
|
||||
await check1WebSeed(strategy, video1Server2UUID)
|
||||
await check2Webseeds(strategy, video2Server2UUID)
|
||||
checked = true
|
||||
} catch {
|
||||
checked = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'mocha'
|
|||
import { JobState, Video } from '../../../../shared/models'
|
||||
import { VideoPrivacy } from '../../../../shared/models/videos'
|
||||
import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
|
||||
|
||||
import {
|
||||
completeVideoCheck,
|
||||
getVideo,
|
||||
|
@ -18,6 +19,7 @@ import {
|
|||
ServerInfo,
|
||||
setAccessTokensToServers,
|
||||
uploadVideo,
|
||||
updateVideo,
|
||||
wait
|
||||
} from '../../../../shared/utils'
|
||||
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)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
const res1 = await getVideosList(servers[2].url)
|
||||
expect(res1.body.data).to.be.an('array')
|
||||
expect(res1.body.data).to.have.lengthOf(11)
|
||||
|
||||
await viewVideo(servers[0].url, missedVideo1.uuid)
|
||||
await viewVideo(servers[0].url, unlistedVideo.uuid)
|
||||
await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, { })
|
||||
await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, { })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
|
|
|
@ -6,3 +6,4 @@ import './jobs'
|
|||
import './reverse-proxy'
|
||||
import './stats'
|
||||
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 () {
|
||||
this.timeout(10000)
|
||||
|
||||
await execCLI(cmd + ` auth del ${server.url}`)
|
||||
|
||||
killallServers([ server ])
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Order of the tests we want to execute
|
||||
import './client'
|
||||
import './activitypub'
|
||||
import './feeds/'
|
||||
import './cli/'
|
||||
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 { isUserUsernameValid } from '../helpers/custom-validators/users'
|
||||
|
||||
function delInstance (url: string) {
|
||||
return new Promise((res, rej): void => {
|
||||
getSettings()
|
||||
.then(async (settings) => {
|
||||
settings.remotes.splice(settings.remotes.indexOf(url))
|
||||
await writeSettings(settings)
|
||||
delete netrc.machines[url]
|
||||
netrc.save()
|
||||
res()
|
||||
})
|
||||
.catch(err => rej(err))
|
||||
})
|
||||
async function delInstance (url: string) {
|
||||
const settings = await getSettings()
|
||||
|
||||
settings.remotes.splice(settings.remotes.indexOf(url))
|
||||
await writeSettings(settings)
|
||||
|
||||
delete netrc.machines[url]
|
||||
await netrc.save()
|
||||
}
|
||||
|
||||
async function setInstance (url: string, username: string, password: string) {
|
||||
return new Promise((res, rej): void => {
|
||||
getSettings()
|
||||
.then(async settings => {
|
||||
if (settings.remotes.indexOf(url) === -1) {
|
||||
settings.remotes.push(url)
|
||||
}
|
||||
await writeSettings(settings)
|
||||
netrc.machines[url] = { login: username, password }
|
||||
netrc.save()
|
||||
res()
|
||||
})
|
||||
.catch(err => rej(err))
|
||||
})
|
||||
const settings = await getSettings()
|
||||
if (settings.remotes.indexOf(url) === -1) {
|
||||
settings.remotes.push(url)
|
||||
}
|
||||
await writeSettings(settings)
|
||||
|
||||
netrc.machines[url] = { login: username, password }
|
||||
await netrc.save()
|
||||
}
|
||||
|
||||
function isURLaPeerTubeInstance (url: string) {
|
||||
|
@ -71,56 +62,60 @@ program
|
|||
required: true
|
||||
}
|
||||
}
|
||||
}, (_, result) => {
|
||||
setInstance(result.url, result.username, result.password)
|
||||
}, async (_, result) => {
|
||||
await setInstance(result.url, result.username, result.password)
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
|
||||
program
|
||||
.command('del <url>')
|
||||
.description('unregisters a remote instance')
|
||||
.action((url) => {
|
||||
delInstance(url)
|
||||
.action(async url => {
|
||||
await delInstance(url)
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('lists registered remote instances')
|
||||
.action(() => {
|
||||
getSettings()
|
||||
.then(settings => {
|
||||
const table = new Table({
|
||||
head: ['instance', 'login'],
|
||||
colWidths: [30, 30]
|
||||
})
|
||||
netrc.loadSync()
|
||||
settings.remotes.forEach(element => {
|
||||
table.push([
|
||||
element,
|
||||
netrc.machines[element].login
|
||||
])
|
||||
})
|
||||
.action(async () => {
|
||||
const settings = await getSettings()
|
||||
const table = new Table({
|
||||
head: ['instance', 'login'],
|
||||
colWidths: [30, 30]
|
||||
})
|
||||
netrc.loadSync()
|
||||
settings.remotes.forEach(element => {
|
||||
table.push([
|
||||
element,
|
||||
netrc.machines[element].login
|
||||
])
|
||||
})
|
||||
|
||||
console.log(table.toString())
|
||||
})
|
||||
console.log(table.toString())
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
program
|
||||
.command('set-default <url>')
|
||||
.description('set an existing entry as default')
|
||||
.action((url) => {
|
||||
getSettings()
|
||||
.then(settings => {
|
||||
const instanceExists = settings.remotes.indexOf(url) !== -1
|
||||
.action(async url => {
|
||||
const settings = await getSettings()
|
||||
const instanceExists = settings.remotes.indexOf(url) !== -1
|
||||
|
||||
if (instanceExists) {
|
||||
settings.default = settings.remotes.indexOf(url)
|
||||
writeSettings(settings)
|
||||
} else {
|
||||
console.log('<url> is not a registered instance.')
|
||||
process.exit(-1)
|
||||
}
|
||||
})
|
||||
if (instanceExists) {
|
||||
settings.default = settings.remotes.indexOf(url)
|
||||
await writeSettings(settings)
|
||||
|
||||
process.exit(0)
|
||||
} else {
|
||||
console.log('<url> is not a registered instance.')
|
||||
process.exit(-1)
|
||||
}
|
||||
})
|
||||
|
||||
program.on('--help', function () {
|
||||
|
|
|
@ -58,7 +58,7 @@ if (!process.argv.slice(2).length) {
|
|||
,"\\/
|
||||
_,.__/"\\/_ (the CLI for red chocobos)
|
||||
/ \\) "./, ".
|
||||
--/---"---" "-) )---- by Chocobozzz et al.`)
|
||||
--/---"---" "-) )---- by Chocobozzz et al.\n`)
|
||||
}
|
||||
|
||||
getSettings()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export interface DislikeObject {
|
||||
type: 'Dislike',
|
||||
id: string
|
||||
type: 'Dislike'
|
||||
actor: 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