]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Implement remote interaction
authorChocobozzz <me@florianbigard.com>
Thu, 14 Jan 2021 13:13:23 +0000 (14:13 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 15 Jan 2021 09:49:10 +0000 (10:49 +0100)
14 files changed:
client/src/app/+remote-interaction/remote-interaction-routing.module.ts [new file with mode: 0644]
client/src/app/+remote-interaction/remote-interaction.component.html [new file with mode: 0644]
client/src/app/+remote-interaction/remote-interaction.component.scss [new file with mode: 0644]
client/src/app/+remote-interaction/remote-interaction.component.ts [new file with mode: 0644]
client/src/app/+remote-interaction/remote-interaction.module.ts [new file with mode: 0644]
client/src/app/+videos/+video-watch/comment/video-comment-add.component.html
client/src/app/app-routing.module.ts
client/src/app/core/routing/redirect.service.ts
client/src/app/shared/form-validators/user-validators.ts
client/src/app/shared/shared-user-subscription/remote-subscribe.component.html
client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
client/src/app/shared/shared-user-subscription/subscribe-button.component.html
server/controllers/webfinger.ts
server/tests/misc-endpoints.ts

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 (file)
index 0000000..1dddfb3
--- /dev/null
@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+import { LoginGuard } from '@app/core'
+import { RemoteInteractionComponent } from './remote-interaction.component'
+
+const remoteInteractionRoutes: Routes = [
+  {
+    path: '',
+    component: RemoteInteractionComponent,
+    canActivate: [ LoginGuard ],
+    data: {
+      meta: {
+        title: $localize`Remote interaction`
+      }
+    }
+  }
+]
+
+@NgModule({
+  imports: [ RouterModule.forChild(remoteInteractionRoutes) ],
+  exports: [ RouterModule ]
+})
+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 (file)
index 0000000..e59783b
--- /dev/null
@@ -0,0 +1,7 @@
+<div class="root">
+
+  <div class="alert alert-error" *ngIf="error">
+    {{ error }}
+  </div>
+
+</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 (file)
index 0000000..5e67747
--- /dev/null
@@ -0,0 +1,2 @@
+@import '_variables';
+@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 (file)
index 0000000..e24607b
--- /dev/null
@@ -0,0 +1,56 @@
+import { forkJoin } from 'rxjs'
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { VideoChannel } from '@app/shared/shared-main'
+import { SearchService } from '@app/shared/shared-search'
+
+@Component({
+  selector: 'my-remote-interaction',
+  templateUrl: './remote-interaction.component.html',
+  styleUrls: ['./remote-interaction.component.scss']
+})
+export class RemoteInteractionComponent implements OnInit {
+  error = ''
+
+  constructor (
+    private route: ActivatedRoute,
+    private router: Router,
+    private search: SearchService
+  ) { }
+
+  ngOnInit () {
+    const uri = this.route.snapshot.queryParams['uri']
+
+    if (!uri) {
+      this.error = $localize`URL parameter is missing in URL parameters`
+      return
+    }
+
+    this.loadUrl(uri)
+  }
+
+  private loadUrl (uri: string) {
+    forkJoin([
+      this.search.searchVideos({ search: uri }),
+      this.search.searchVideoChannels({ search: uri })
+    ]).subscribe(([ videoResult, channelResult ]) => {
+      let redirectUrl: string
+
+      if (videoResult.data.length !== 0) {
+        const video = videoResult.data[0]
+
+        redirectUrl = '/videos/watch/' + video.uuid
+      } else if (channelResult.data.length !== 0) {
+        const channel = new VideoChannel(channelResult.data[0])
+
+        redirectUrl = '/video-channels/' + channel.nameWithHost
+      } else {
+        this.error = $localize`Cannot access to the remote resource`
+        return
+      }
+
+      this.router.navigateByUrl(redirectUrl)
+    })
+  }
+
+}
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 (file)
index 0000000..9f9f1cd
--- /dev/null
@@ -0,0 +1,26 @@
+import { CommonModule } from '@angular/common'
+import { NgModule } from '@angular/core'
+import { SharedSearchModule } from '@app/shared/shared-search'
+import { RemoteInteractionRoutingModule } from './remote-interaction-routing.module'
+import { RemoteInteractionComponent } from './remote-interaction.component'
+
+@NgModule({
+  imports: [
+    CommonModule,
+
+    SharedSearchModule,
+
+    RemoteInteractionRoutingModule
+  ],
+
+  declarations: [
+    RemoteInteractionComponent
+  ],
+
+  exports: [
+    RemoteInteractionComponent
+  ],
+
+  providers: []
+})
+export class RemoteInteractionModule { }
index ca9cd863bed1eb2de88ad5a61811acbc8ee08350..fdefed09a57fad931a588543e4f61d7b3de1613b 100644 (file)
   </div>
   <div class="modal-body">
     <span i18n>
-      You can comment using an account on any ActivityPub-compatible instance.
-      On most platforms, you can find the video by typing its URL in the search bar and then comment it
-      from within the software's interface.
-    </span>
-    <span i18n>
-      If you have an account on Mastodon or Pleroma, you can open it directly in their interface:
+      You can comment using an account on any ActivityPub-compatible instance (PeerTube/Mastodon/Pleroma account for example).
     </span>
+
     <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe>
   </div>
   <div class="modal-footer inputs">
index bcae29c9a3e962087308c33cbd2d034082a7d4c3..7a55a7b8dc7b837a23ee062c09bba7e46c6c9884 100644 (file)
@@ -2,9 +2,9 @@ import { NgModule } from '@angular/core'
 import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
 import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
 import { MenuGuards } from '@app/core/routing/menu-guard.service'
+import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
 import { PreloadSelectedModulesList } from './core'
 import { EmptyComponent } from './empty.component'
-import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
 
 const routes: Routes = [
   {
@@ -57,6 +57,10 @@ const routes: Routes = [
     path: 'videos',
     loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule)
   },
+  {
+    path: 'remote-interaction',
+    loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule)
+  },
   {
     path: '',
     component: EmptyComponent // Avoid 404, app component will redirect dynamically
index 4f4b346e2f0f93b89a1740b53607f22d26f0cf7e..3218040bf1c43479ad617be79b7323f288aeab95 100644 (file)
@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core'
-import { NavigationEnd, Router } from '@angular/router'
+import { NavigationCancel, NavigationEnd, Router } from '@angular/router'
 import { ServerService } from '../server'
 
 @Injectable()
@@ -36,7 +36,7 @@ export class RedirectService {
     // Track previous url
     this.currentUrl = this.router.url
     router.events.subscribe(event => {
-      if (event instanceof NavigationEnd) {
+      if (event instanceof NavigationEnd || event instanceof NavigationCancel) {
         this.previousUrl = this.currentUrl
         this.currentUrl = event.url
       }
index a272900358889eddeb73c2a10dba271d441b63b9..3f462ca3b690ccff7d4ed49c1bcafb9fba8c89be 100644 (file)
@@ -39,6 +39,17 @@ export const USER_EMAIL_VALIDATOR: BuildFormValidator = {
   }
 }
 
+export const USER_HANDLE_VALIDATOR: BuildFormValidator = {
+  VALIDATORS: [
+    Validators.required,
+    Validators.pattern(/@.+/)
+  ],
+  MESSAGES: {
+    'required': $localize`Handle is required.`,
+    'pattern': $localize`Handle must be valid (chocobozzz@example.com).`
+  }
+}
+
 export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
   VALIDATORS: [
     Validators.required
index acfec0a8e9014a64840eed52121357192dc4ffec..a00c3d1c79991b5ac36f5aa8c43ea466f9ca2c9a 100644 (file)
@@ -15,8 +15,7 @@
   <my-help *ngIf="!interact && showHelp">
     <ng-template ptTemplate="customHtml">
       <ng-container i18n>
-        You can subscribe to the channel via any ActivityPub-capable fediverse instance.<br /><br />
-        For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.
+        You can subscribe to the channel via any ActivityPub-capable fediverse instance (PeerTube, Mastodon or Pleroma for example).
       </ng-container>
     </ng-template>
   </my-help>
@@ -24,8 +23,7 @@
   <my-help *ngIf="showHelp && interact">
     <ng-template ptTemplate="customHtml">
       <ng-container i18n>
-        You can interact with this via any ActivityPub-capable fediverse instance.<br /><br />
-        For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.
+        You can interact with this via any ActivityPub-capable fediverse instance (PeerTube, Mastodon or Pleroma for example).
       </ng-container>
     </ng-template>
   </my-help>
index b46c91bf8bd496a1352c8992575841016c17c2aa..66619952343be7296788178fbbe8d879f0b34ad7 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, Input, OnInit } from '@angular/core'
+import { Notifier } from '@app/core'
 import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
-import { USER_EMAIL_VALIDATOR } from '../form-validators/user-validators'
+import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators'
 
 @Component({
   selector: 'my-remote-subscribe',
@@ -13,14 +14,15 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
   @Input() showHelp = false
 
   constructor (
-    protected formValidatorService: FormValidatorService
+    protected formValidatorService: FormValidatorService,
+    private notifier: Notifier
   ) {
     super()
   }
 
   ngOnInit () {
     this.buildForm({
-      text: USER_EMAIL_VALIDATOR
+      text: USER_HANDLE_VALIDATOR
     })
   }
 
@@ -35,22 +37,27 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
     const address = this.form.value['text']
     const [ username, hostname ] = address.split('@')
 
+    const protocol = window.location.protocol
+
     // Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5
-    fetch(`https://${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`)
+    fetch(`${protocol}//${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`)
       .then(response => response.json())
-      .then(data => new Promise((resolve, reject) => {
-        if (data && Array.isArray(data.links)) {
-          const link: { template: string } = data.links.find((link: any) => {
-            return link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe'
-          })
-
-          if (link && link.template.includes('{uri}')) {
-            resolve(link.template.replace('{uri}', encodeURIComponent(this.uri)))
-          }
+      .then(data => new Promise((res, rej) => {
+        if (!data || Array.isArray(data.links) === false) return rej()
+
+        const link: { template: string } = data.links.find((link: any) => {
+          return link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe'
+        })
+
+        if (link && link.template.includes('{uri}')) {
+          res(link.template.replace('{uri}', encodeURIComponent(this.uri)))
         }
-        reject()
       }))
       .then(window.open)
-      .catch(err => console.error(err))
+      .catch(err => {
+        console.error(err)
+
+        this.notifier.error($localize`Cannot fetch information of this remote account`)
+      })
   }
 }
index 607a7e11357038def6b953e3d8de982774b309ba..75cfc918b5c0c4d46ca7af2b0dae7b6186b05c65 100644 (file)
@@ -59,7 +59,7 @@
       </button>
 
       <button class="dropdown-item dropdown-item-neutral">
-        <div class="mb-1" i18n>Subscribe with a Mastodon account:</div>
+        <div class="mb-1" i18n>Subscribe with a remote account:</div>
         <my-remote-subscribe [showHelp]="true" [uri]="uri"></my-remote-subscribe>
       </button>
 
index 5c308d9ad1f0b8a8dcce864e6a4121a830ff7110..885e4498f069c02df78f0114235b0339487d3fd3 100644 (file)
@@ -1,5 +1,6 @@
 import * as cors from 'cors'
 import * as express from 'express'
+import { WEBSERVER } from '@server/initializers/constants'
 import { asyncMiddleware } from '../middlewares'
 import { webfingerValidator } from '../middlewares/validators'
 
@@ -31,6 +32,10 @@ function webfingerController (req: express.Request, res: express.Response) {
         rel: 'self',
         type: 'application/activity+json',
         href: actor.url
+      },
+      {
+        rel: 'http://ostatus.org/schema/1.0/subscribe',
+        template: WEBSERVER.URL + '/remote-interaction?uri={uri}'
       }
     ]
   }
index 162b53e18d6b7f6ab2e0e0960bcdcdd4d117f051..09e5afcf9b4bc6c9fc58eb3032638f3798d520da 100644 (file)
@@ -80,6 +80,31 @@ describe('Test misc endpoints', function () {
 
       expect(res.header.location).to.equal('/my-account/settings')
     })
+
+    it('Should test webfinger', async function () {
+      const resource = 'acct:peertube@' + server.host
+      const accountUrl = server.url + '/accounts/peertube'
+
+      const res = await makeGetRequest({
+        url: server.url,
+        path: '/.well-known/webfinger?resource=' + resource,
+        statusCodeExpected: HttpStatusCode.OK_200
+      })
+
+      const data = res.body
+
+      expect(data.subject).to.equal(resource)
+      expect(data.aliases).to.contain(accountUrl)
+
+      const self = data.links.find(l => l.rel === 'self')
+      expect(self).to.exist
+      expect(self.type).to.equal('application/activity+json')
+      expect(self.href).to.equal(accountUrl)
+
+      const remoteInteract = data.links.find(l => l.rel === 'http://ostatus.org/schema/1.0/subscribe')
+      expect(remoteInteract).to.exist
+      expect(remoteInteract.template).to.equal(server.url + '/remote-interaction?uri={uri}')
+    })
   })
 
   describe('Test classic static endpoints', function () {