mirror of https://github.com/Chocobozzz/PeerTube
Implement remote interaction
parent
b0a9743af0
commit
d43c6b1ffc
|
@ -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 {}
|
|
@ -0,0 +1,7 @@
|
|||
<div class="root">
|
||||
|
||||
<div class="alert alert-error" *ngIf="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,2 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
|
@ -57,13 +57,9 @@
|
|||
</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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('@')
|
||||
|
||||
// Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5
|
||||
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'
|
||||
})
|
||||
const protocol = window.location.protocol
|
||||
|
||||
if (link && link.template.includes('{uri}')) {
|
||||
resolve(link.template.replace('{uri}', encodeURIComponent(this.uri)))
|
||||
}
|
||||
// Should not have CORS error because https://tools.ietf.org/html/rfc7033#section-5
|
||||
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)
|
||||
.catch(err => console.error(err))
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
|
||||
this.notifier.error($localize`Cannot fetch information of this remote account`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
Loading…
Reference in New Issue