Implement remote interaction

This commit is contained in:
Chocobozzz 2021-01-14 14:13:23 +01:00 committed by Chocobozzz
parent b0a9743af0
commit d43c6b1ffc
14 changed files with 190 additions and 30 deletions

View File

@ -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 {}

View File

@ -0,0 +1,7 @@
<div class="root">
<div class="alert alert-error" *ngIf="error">
{{ error }}
</div>
</div>

View File

@ -0,0 +1,2 @@
@import '_variables';
@import '_mixins';

View File

@ -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)
})
}
}

View File

@ -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 { }

View File

@ -57,13 +57,9 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<span i18n> <span i18n>
You can comment using an account on any ActivityPub-compatible instance. You can comment using an account on any ActivityPub-compatible instance (PeerTube/Mastodon/Pleroma account for example).
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:
</span> </span>
<my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe> <my-remote-subscribe [interact]="true" [uri]="getUri()"></my-remote-subscribe>
</div> </div>
<div class="modal-footer inputs"> <div class="modal-footer inputs">

View File

@ -2,9 +2,9 @@ import { NgModule } from '@angular/core'
import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
import { MenuGuards } from '@app/core/routing/menu-guard.service' import { MenuGuards } from '@app/core/routing/menu-guard.service'
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
import { PreloadSelectedModulesList } from './core' import { PreloadSelectedModulesList } from './core'
import { EmptyComponent } from './empty.component' import { EmptyComponent } from './empty.component'
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
const routes: Routes = [ const routes: Routes = [
{ {
@ -57,6 +57,10 @@ const routes: Routes = [
path: 'videos', path: 'videos',
loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule) loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule)
}, },
{
path: 'remote-interaction',
loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule)
},
{ {
path: '', path: '',
component: EmptyComponent // Avoid 404, app component will redirect dynamically component: EmptyComponent // Avoid 404, app component will redirect dynamically

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { NavigationEnd, Router } from '@angular/router' import { NavigationCancel, NavigationEnd, Router } from '@angular/router'
import { ServerService } from '../server' import { ServerService } from '../server'
@Injectable() @Injectable()
@ -36,7 +36,7 @@ export class RedirectService {
// Track previous url // Track previous url
this.currentUrl = this.router.url this.currentUrl = this.router.url
router.events.subscribe(event => { router.events.subscribe(event => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd || event instanceof NavigationCancel) {
this.previousUrl = this.currentUrl this.previousUrl = this.currentUrl
this.currentUrl = event.url this.currentUrl = event.url
} }

View 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 = { export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
VALIDATORS: [ VALIDATORS: [
Validators.required Validators.required

View File

@ -15,8 +15,7 @@
<my-help *ngIf="!interact && showHelp"> <my-help *ngIf="!interact && showHelp">
<ng-template ptTemplate="customHtml"> <ng-template ptTemplate="customHtml">
<ng-container i18n> <ng-container i18n>
You can subscribe to the channel via any ActivityPub-capable fediverse instance.<br /><br /> You can subscribe to the channel via any ActivityPub-capable fediverse instance (PeerTube, Mastodon or Pleroma for example).
For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.
</ng-container> </ng-container>
</ng-template> </ng-template>
</my-help> </my-help>
@ -24,8 +23,7 @@
<my-help *ngIf="showHelp && interact"> <my-help *ngIf="showHelp && interact">
<ng-template ptTemplate="customHtml"> <ng-template ptTemplate="customHtml">
<ng-container i18n> <ng-container i18n>
You can interact with this via any ActivityPub-capable fediverse instance.<br /><br /> You can interact with this via any ActivityPub-capable fediverse instance (PeerTube, Mastodon or Pleroma for example).
For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.
</ng-container> </ng-container>
</ng-template> </ng-template>
</my-help> </my-help>

View File

@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core' import { Component, Input, OnInit } from '@angular/core'
import { Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 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({ @Component({
selector: 'my-remote-subscribe', selector: 'my-remote-subscribe',
@ -13,14 +14,15 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
@Input() showHelp = false @Input() showHelp = false
constructor ( constructor (
protected formValidatorService: FormValidatorService protected formValidatorService: FormValidatorService,
private notifier: Notifier
) { ) {
super() super()
} }
ngOnInit () { ngOnInit () {
this.buildForm({ 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 address = this.form.value['text']
const [ username, hostname ] = address.split('@') const [ username, hostname ] = address.split('@')
// Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5 const protocol = window.location.protocol
fetch(`https://${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}')) { // Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5
resolve(link.template.replace('{uri}', encodeURIComponent(this.uri))) fetch(`${protocol}//${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`)
} .then(response => response.json())
.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) .then(window.open)
.catch(err => console.error(err)) .catch(err => {
console.error(err)
this.notifier.error($localize`Cannot fetch information of this remote account`)
})
} }
} }

View File

@ -59,7 +59,7 @@
</button> </button>
<button class="dropdown-item dropdown-item-neutral"> <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> <my-remote-subscribe [showHelp]="true" [uri]="uri"></my-remote-subscribe>
</button> </button>

View File

@ -1,5 +1,6 @@
import * as cors from 'cors' import * as cors from 'cors'
import * as express from 'express' import * as express from 'express'
import { WEBSERVER } from '@server/initializers/constants'
import { asyncMiddleware } from '../middlewares' import { asyncMiddleware } from '../middlewares'
import { webfingerValidator } from '../middlewares/validators' import { webfingerValidator } from '../middlewares/validators'
@ -31,6 +32,10 @@ function webfingerController (req: express.Request, res: express.Response) {
rel: 'self', rel: 'self',
type: 'application/activity+json', type: 'application/activity+json',
href: actor.url href: actor.url
},
{
rel: 'http://ostatus.org/schema/1.0/subscribe',
template: WEBSERVER.URL + '/remote-interaction?uri={uri}'
} }
] ]
} }

View File

@ -80,6 +80,31 @@ describe('Test misc endpoints', function () {
expect(res.header.location).to.equal('/my-account/settings') 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 () { describe('Test classic static endpoints', function () {