diff options
author | Chocobozzz <me@florianbigard.com> | 2021-01-14 14:13:23 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-01-15 10:49:10 +0100 |
commit | d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20 (patch) | |
tree | 6bdcbe9893574e0b5a41c4854c7f986f346ba761 | |
parent | b0a9743af0273835cdf594431a774c0f7d46b539 (diff) | |
download | PeerTube-d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20.tar.gz PeerTube-d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20.tar.zst PeerTube-d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20.zip |
Implement remote interaction
14 files changed, 189 insertions, 29 deletions
diff --git a/client/src/app/+remote-interaction/remote-interaction-routing.module.ts b/client/src/app/+remote-interaction/remote-interaction-routing.module.ts new file mode 100644 index 000000000..1dddfb3ba --- /dev/null +++ b/client/src/app/+remote-interaction/remote-interaction-routing.module.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | import { LoginGuard } from '@app/core' | ||
4 | import { RemoteInteractionComponent } from './remote-interaction.component' | ||
5 | |||
6 | const remoteInteractionRoutes: Routes = [ | ||
7 | { | ||
8 | path: '', | ||
9 | component: RemoteInteractionComponent, | ||
10 | canActivate: [ LoginGuard ], | ||
11 | data: { | ||
12 | meta: { | ||
13 | title: $localize`Remote interaction` | ||
14 | } | ||
15 | } | ||
16 | } | ||
17 | ] | ||
18 | |||
19 | @NgModule({ | ||
20 | imports: [ RouterModule.forChild(remoteInteractionRoutes) ], | ||
21 | exports: [ RouterModule ] | ||
22 | }) | ||
23 | export class RemoteInteractionRoutingModule {} | ||
diff --git a/client/src/app/+remote-interaction/remote-interaction.component.html b/client/src/app/+remote-interaction/remote-interaction.component.html new file mode 100644 index 000000000..e59783b9a --- /dev/null +++ b/client/src/app/+remote-interaction/remote-interaction.component.html | |||
@@ -0,0 +1,7 @@ | |||
1 | <div class="root"> | ||
2 | |||
3 | <div class="alert alert-error" *ngIf="error"> | ||
4 | {{ error }} | ||
5 | </div> | ||
6 | |||
7 | </div> | ||
diff --git a/client/src/app/+remote-interaction/remote-interaction.component.scss b/client/src/app/+remote-interaction/remote-interaction.component.scss new file mode 100644 index 000000000..5e6774739 --- /dev/null +++ b/client/src/app/+remote-interaction/remote-interaction.component.scss | |||
@@ -0,0 +1,2 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
diff --git a/client/src/app/+remote-interaction/remote-interaction.component.ts b/client/src/app/+remote-interaction/remote-interaction.component.ts new file mode 100644 index 000000000..e24607b24 --- /dev/null +++ b/client/src/app/+remote-interaction/remote-interaction.component.ts | |||
@@ -0,0 +1,56 @@ | |||
1 | import { forkJoin } from 'rxjs' | ||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { VideoChannel } from '@app/shared/shared-main' | ||
5 | import { SearchService } from '@app/shared/shared-search' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-remote-interaction', | ||
9 | templateUrl: './remote-interaction.component.html', | ||
10 | styleUrls: ['./remote-interaction.component.scss'] | ||
11 | }) | ||
12 | export class RemoteInteractionComponent implements OnInit { | ||
13 | error = '' | ||
14 | |||
15 | constructor ( | ||
16 | private route: ActivatedRoute, | ||
17 | private router: Router, | ||
18 | private search: SearchService | ||
19 | ) { } | ||
20 | |||
21 | ngOnInit () { | ||
22 | const uri = this.route.snapshot.queryParams['uri'] | ||
23 | |||
24 | if (!uri) { | ||
25 | this.error = $localize`URL parameter is missing in URL parameters` | ||
26 | return | ||
27 | } | ||
28 | |||
29 | this.loadUrl(uri) | ||
30 | } | ||
31 | |||
32 | private loadUrl (uri: string) { | ||
33 | forkJoin([ | ||
34 | this.search.searchVideos({ search: uri }), | ||
35 | this.search.searchVideoChannels({ search: uri }) | ||
36 | ]).subscribe(([ videoResult, channelResult ]) => { | ||
37 | let redirectUrl: string | ||
38 | |||
39 | if (videoResult.data.length !== 0) { | ||
40 | const video = videoResult.data[0] | ||
41 | |||
42 | redirectUrl = '/videos/watch/' + video.uuid | ||
43 | } else if (channelResult.data.length !== 0) { | ||
44 | const channel = new VideoChannel(channelResult.data[0]) | ||
45 | |||
46 | redirectUrl = '/video-channels/' + channel.nameWithHost | ||
47 | } else { | ||
48 | this.error = $localize`Cannot access to the remote resource` | ||
49 | return | ||
50 | } | ||
51 | |||
52 | this.router.navigateByUrl(redirectUrl) | ||
53 | }) | ||
54 | } | ||
55 | |||
56 | } | ||
diff --git a/client/src/app/+remote-interaction/remote-interaction.module.ts b/client/src/app/+remote-interaction/remote-interaction.module.ts new file mode 100644 index 000000000..9f9f1cdfd --- /dev/null +++ b/client/src/app/+remote-interaction/remote-interaction.module.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import { CommonModule } from '@angular/common' | ||
2 | import { NgModule } from '@angular/core' | ||
3 | import { SharedSearchModule } from '@app/shared/shared-search' | ||
4 | import { RemoteInteractionRoutingModule } from './remote-interaction-routing.module' | ||
5 | import { RemoteInteractionComponent } from './remote-interaction.component' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | CommonModule, | ||
10 | |||
11 | SharedSearchModule, | ||
12 | |||
13 | RemoteInteractionRoutingModule | ||
14 | ], | ||
15 | |||
16 | declarations: [ | ||
17 | RemoteInteractionComponent | ||
18 | ], | ||
19 | |||
20 | exports: [ | ||
21 | RemoteInteractionComponent | ||
22 | ], | ||
23 | |||
24 | providers: [] | ||
25 | }) | ||
26 | export class RemoteInteractionModule { } | ||
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html index ca9cd863b..fdefed09a 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html +++ b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.html | |||
@@ -57,13 +57,9 @@ | |||
57 | </div> | 57 | </div> |
58 | <div class="modal-body"> | 58 | <div class="modal-body"> |
59 | <span i18n> | 59 | <span i18n> |
60 | You can comment using an account on any ActivityPub-compatible instance. | 60 | You can comment using an account on any ActivityPub-compatible instance (PeerTube/Mastodon/Pleroma account for example). |
61 | On most platforms, you can find the video by typing its URL in the search bar and then comment it | ||
62 | from within the software's interface. | ||
63 | </span> | ||
64 | <span i18n> | ||
65 | If you have an account on Mastodon or Pleroma, you can open it directly in their interface: | ||
66 | </span> | 61 | </span> |
62 | |||
67 | <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe> | 63 | <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe> |
68 | </div> | 64 | </div> |
69 | <div class="modal-footer inputs"> | 65 | <div class="modal-footer inputs"> |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index bcae29c9a..7a55a7b8d 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -2,9 +2,9 @@ import { NgModule } from '@angular/core' | |||
2 | import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' | 2 | import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' |
3 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' | 3 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' |
4 | import { MenuGuards } from '@app/core/routing/menu-guard.service' | 4 | import { MenuGuards } from '@app/core/routing/menu-guard.service' |
5 | import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' | ||
5 | import { PreloadSelectedModulesList } from './core' | 6 | import { PreloadSelectedModulesList } from './core' |
6 | import { EmptyComponent } from './empty.component' | 7 | import { EmptyComponent } from './empty.component' |
7 | import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' | ||
8 | 8 | ||
9 | const routes: Routes = [ | 9 | const routes: Routes = [ |
10 | { | 10 | { |
@@ -58,6 +58,10 @@ const routes: Routes = [ | |||
58 | loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule) | 58 | loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule) |
59 | }, | 59 | }, |
60 | { | 60 | { |
61 | path: 'remote-interaction', | ||
62 | loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule) | ||
63 | }, | ||
64 | { | ||
61 | path: '', | 65 | path: '', |
62 | component: EmptyComponent // Avoid 404, app component will redirect dynamically | 66 | component: EmptyComponent // Avoid 404, app component will redirect dynamically |
63 | } | 67 | } |
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts index 4f4b346e2..3218040bf 100644 --- a/client/src/app/core/routing/redirect.service.ts +++ b/client/src/app/core/routing/redirect.service.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { NavigationEnd, Router } from '@angular/router' | 2 | import { NavigationCancel, NavigationEnd, Router } from '@angular/router' |
3 | import { ServerService } from '../server' | 3 | import { ServerService } from '../server' |
4 | 4 | ||
5 | @Injectable() | 5 | @Injectable() |
@@ -36,7 +36,7 @@ export class RedirectService { | |||
36 | // Track previous url | 36 | // Track previous url |
37 | this.currentUrl = this.router.url | 37 | this.currentUrl = this.router.url |
38 | router.events.subscribe(event => { | 38 | router.events.subscribe(event => { |
39 | if (event instanceof NavigationEnd) { | 39 | if (event instanceof NavigationEnd || event instanceof NavigationCancel) { |
40 | this.previousUrl = this.currentUrl | 40 | this.previousUrl = this.currentUrl |
41 | this.currentUrl = event.url | 41 | this.currentUrl = event.url |
42 | } | 42 | } |
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts index a27290035..3f462ca3b 100644 --- a/client/src/app/shared/form-validators/user-validators.ts +++ b/client/src/app/shared/form-validators/user-validators.ts | |||
@@ -39,6 +39,17 @@ export const USER_EMAIL_VALIDATOR: BuildFormValidator = { | |||
39 | } | 39 | } |
40 | } | 40 | } |
41 | 41 | ||
42 | export const USER_HANDLE_VALIDATOR: BuildFormValidator = { | ||
43 | VALIDATORS: [ | ||
44 | Validators.required, | ||
45 | Validators.pattern(/@.+/) | ||
46 | ], | ||
47 | MESSAGES: { | ||
48 | 'required': $localize`Handle is required.`, | ||
49 | 'pattern': $localize`Handle must be valid (chocobozzz@example.com).` | ||
50 | } | ||
51 | } | ||
52 | |||
42 | export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = { | 53 | export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = { |
43 | VALIDATORS: [ | 54 | VALIDATORS: [ |
44 | Validators.required | 55 | Validators.required |
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html index acfec0a8e..a00c3d1c7 100644 --- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html +++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.html | |||
@@ -15,8 +15,7 @@ | |||
15 | <my-help *ngIf="!interact && showHelp"> | 15 | <my-help *ngIf="!interact && showHelp"> |
16 | <ng-template ptTemplate="customHtml"> | 16 | <ng-template ptTemplate="customHtml"> |
17 | <ng-container i18n> | 17 | <ng-container i18n> |
18 | You can subscribe to the channel via any ActivityPub-capable fediverse instance.<br /><br /> | 18 | You can subscribe to the channel via any ActivityPub-capable fediverse instance (PeerTube, Mastodon or Pleroma for example). |
19 | For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there. | ||
20 | </ng-container> | 19 | </ng-container> |
21 | </ng-template> | 20 | </ng-template> |
22 | </my-help> | 21 | </my-help> |
@@ -24,8 +23,7 @@ | |||
24 | <my-help *ngIf="showHelp && interact"> | 23 | <my-help *ngIf="showHelp && interact"> |
25 | <ng-template ptTemplate="customHtml"> | 24 | <ng-template ptTemplate="customHtml"> |
26 | <ng-container i18n> | 25 | <ng-container i18n> |
27 | You can interact with this via any ActivityPub-capable fediverse instance.<br /><br /> | 26 | You can interact with this via any ActivityPub-capable fediverse instance (PeerTube, Mastodon or Pleroma for example). |
28 | For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there. | ||
29 | </ng-container> | 27 | </ng-container> |
30 | </ng-template> | 28 | </ng-template> |
31 | </my-help> | 29 | </my-help> |
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts index b46c91bf8..666199523 100644 --- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts +++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { Notifier } from '@app/core' | ||
2 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
3 | import { USER_EMAIL_VALIDATOR } from '../form-validators/user-validators' | 4 | import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators' |
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-remote-subscribe', | 7 | selector: 'my-remote-subscribe', |
@@ -13,14 +14,15 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit { | |||
13 | @Input() showHelp = false | 14 | @Input() showHelp = false |
14 | 15 | ||
15 | constructor ( | 16 | constructor ( |
16 | protected formValidatorService: FormValidatorService | 17 | protected formValidatorService: FormValidatorService, |
18 | private notifier: Notifier | ||
17 | ) { | 19 | ) { |
18 | super() | 20 | super() |
19 | } | 21 | } |
20 | 22 | ||
21 | ngOnInit () { | 23 | ngOnInit () { |
22 | this.buildForm({ | 24 | this.buildForm({ |
23 | text: USER_EMAIL_VALIDATOR | 25 | text: USER_HANDLE_VALIDATOR |
24 | }) | 26 | }) |
25 | } | 27 | } |
26 | 28 | ||
@@ -35,22 +37,27 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit { | |||
35 | const address = this.form.value['text'] | 37 | const address = this.form.value['text'] |
36 | const [ username, hostname ] = address.split('@') | 38 | const [ username, hostname ] = address.split('@') |
37 | 39 | ||
40 | const protocol = window.location.protocol | ||
41 | |||
38 | // Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5 | 42 | // Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5 |
39 | fetch(`https://${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`) | 43 | fetch(`${protocol}//${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`) |
40 | .then(response => response.json()) | 44 | .then(response => response.json()) |
41 | .then(data => new Promise((resolve, reject) => { | 45 | .then(data => new Promise((res, rej) => { |
42 | if (data && Array.isArray(data.links)) { | 46 | if (!data || Array.isArray(data.links) === false) return rej() |
43 | const link: { template: string } = data.links.find((link: any) => { | 47 | |
44 | return link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe' | 48 | const link: { template: string } = data.links.find((link: any) => { |
45 | }) | 49 | return link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe' |
46 | 50 | }) | |
47 | if (link && link.template.includes('{uri}')) { | 51 | |
48 | resolve(link.template.replace('{uri}', encodeURIComponent(this.uri))) | 52 | if (link && link.template.includes('{uri}')) { |
49 | } | 53 | res(link.template.replace('{uri}', encodeURIComponent(this.uri))) |
50 | } | 54 | } |
51 | reject() | ||
52 | })) | 55 | })) |
53 | .then(window.open) | 56 | .then(window.open) |
54 | .catch(err => console.error(err)) | 57 | .catch(err => { |
58 | console.error(err) | ||
59 | |||
60 | this.notifier.error($localize`Cannot fetch information of this remote account`) | ||
61 | }) | ||
55 | } | 62 | } |
56 | } | 63 | } |
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html index 607a7e113..75cfc918b 100644 --- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html +++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html | |||
@@ -59,7 +59,7 @@ | |||
59 | </button> | 59 | </button> |
60 | 60 | ||
61 | <button class="dropdown-item dropdown-item-neutral"> | 61 | <button class="dropdown-item dropdown-item-neutral"> |
62 | <div class="mb-1" i18n>Subscribe with a Mastodon account:</div> | 62 | <div class="mb-1" i18n>Subscribe with a remote account:</div> |
63 | <my-remote-subscribe [showHelp]="true" [uri]="uri"></my-remote-subscribe> | 63 | <my-remote-subscribe [showHelp]="true" [uri]="uri"></my-remote-subscribe> |
64 | </button> | 64 | </button> |
65 | 65 | ||
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts index 5c308d9ad..885e4498f 100644 --- a/server/controllers/webfinger.ts +++ b/server/controllers/webfinger.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { asyncMiddleware } from '../middlewares' | 4 | import { asyncMiddleware } from '../middlewares' |
4 | import { webfingerValidator } from '../middlewares/validators' | 5 | import { webfingerValidator } from '../middlewares/validators' |
5 | 6 | ||
@@ -31,6 +32,10 @@ function webfingerController (req: express.Request, res: express.Response) { | |||
31 | rel: 'self', | 32 | rel: 'self', |
32 | type: 'application/activity+json', | 33 | type: 'application/activity+json', |
33 | href: actor.url | 34 | href: actor.url |
35 | }, | ||
36 | { | ||
37 | rel: 'http://ostatus.org/schema/1.0/subscribe', | ||
38 | template: WEBSERVER.URL + '/remote-interaction?uri={uri}' | ||
34 | } | 39 | } |
35 | ] | 40 | ] |
36 | } | 41 | } |
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts index 162b53e18..09e5afcf9 100644 --- a/server/tests/misc-endpoints.ts +++ b/server/tests/misc-endpoints.ts | |||
@@ -80,6 +80,31 @@ describe('Test misc endpoints', function () { | |||
80 | 80 | ||
81 | expect(res.header.location).to.equal('/my-account/settings') | 81 | expect(res.header.location).to.equal('/my-account/settings') |
82 | }) | 82 | }) |
83 | |||
84 | it('Should test webfinger', async function () { | ||
85 | const resource = 'acct:peertube@' + server.host | ||
86 | const accountUrl = server.url + '/accounts/peertube' | ||
87 | |||
88 | const res = await makeGetRequest({ | ||
89 | url: server.url, | ||
90 | path: '/.well-known/webfinger?resource=' + resource, | ||
91 | statusCodeExpected: HttpStatusCode.OK_200 | ||
92 | }) | ||
93 | |||
94 | const data = res.body | ||
95 | |||
96 | expect(data.subject).to.equal(resource) | ||
97 | expect(data.aliases).to.contain(accountUrl) | ||
98 | |||
99 | const self = data.links.find(l => l.rel === 'self') | ||
100 | expect(self).to.exist | ||
101 | expect(self.type).to.equal('application/activity+json') | ||
102 | expect(self.href).to.equal(accountUrl) | ||
103 | |||
104 | const remoteInteract = data.links.find(l => l.rel === 'http://ostatus.org/schema/1.0/subscribe') | ||
105 | expect(remoteInteract).to.exist | ||
106 | expect(remoteInteract.template).to.equal(server.url + '/remote-interaction?uri={uri}') | ||
107 | }) | ||
83 | }) | 108 | }) |
84 | 109 | ||
85 | describe('Test classic static endpoints', function () { | 110 | describe('Test classic static endpoints', function () { |