aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-01-14 14:13:23 +0100
committerChocobozzz <chocobozzz@cpy.re>2021-01-15 10:49:10 +0100
commitd43c6b1ffc5e6c895f9e9f9de6625f17a9755c20 (patch)
tree6bdcbe9893574e0b5a41c4854c7f986f346ba761
parentb0a9743af0273835cdf594431a774c0f7d46b539 (diff)
downloadPeerTube-d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20.tar.gz
PeerTube-d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20.tar.zst
PeerTube-d43c6b1ffc5e6c895f9e9f9de6625f17a9755c20.zip
Implement remote interaction
-rw-r--r--client/src/app/+remote-interaction/remote-interaction-routing.module.ts23
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.component.html7
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.component.scss2
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.component.ts56
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.module.ts26
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment-add.component.html8
-rw-r--r--client/src/app/app-routing.module.ts6
-rw-r--r--client/src/app/core/routing/redirect.service.ts4
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts11
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.html6
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts37
-rw-r--r--client/src/app/shared/shared-user-subscription/subscribe-button.component.html2
-rw-r--r--server/controllers/webfinger.ts5
-rw-r--r--server/tests/misc-endpoints.ts25
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 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '@app/core'
4import { RemoteInteractionComponent } from './remote-interaction.component'
5
6const 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})
23export 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 @@
1import { forkJoin } from 'rxjs'
2import { Component, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { VideoChannel } from '@app/shared/shared-main'
5import { 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})
12export 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 @@
1import { CommonModule } from '@angular/common'
2import { NgModule } from '@angular/core'
3import { SharedSearchModule } from '@app/shared/shared-search'
4import { RemoteInteractionRoutingModule } from './remote-interaction-routing.module'
5import { 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})
26export 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'
2import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' 2import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' 3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
4import { MenuGuards } from '@app/core/routing/menu-guard.service' 4import { MenuGuards } from '@app/core/routing/menu-guard.service'
5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
5import { PreloadSelectedModulesList } from './core' 6import { PreloadSelectedModulesList } from './core'
6import { EmptyComponent } from './empty.component' 7import { EmptyComponent } from './empty.component'
7import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
8 8
9const routes: Routes = [ 9const 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 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { NavigationEnd, Router } from '@angular/router' 2import { NavigationCancel, NavigationEnd, Router } from '@angular/router'
3import { ServerService } from '../server' 3import { 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
42export 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
42export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = { 53export 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 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
3import { USER_EMAIL_VALIDATOR } from '../form-validators/user-validators' 4import { 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 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { WEBSERVER } from '@server/initializers/constants'
3import { asyncMiddleware } from '../middlewares' 4import { asyncMiddleware } from '../middlewares'
4import { webfingerValidator } from '../middlewares/validators' 5import { 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 () {