Implement remote interaction
This commit is contained in:
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>
|
||||||
<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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
Loading…
Reference in New Issue