--- /dev/null
+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 {}
--- /dev/null
+<div class="root">
+
+ <div class="alert alert-error" *ngIf="error">
+ {{ error }}
+ </div>
+
+</div>
--- /dev/null
+@import '_variables';
+@import '_mixins';
--- /dev/null
+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)
+ })
+ }
+
+}
--- /dev/null
+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 { }
</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">
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 = [
{
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
import { Injectable } from '@angular/core'
-import { NavigationEnd, Router } from '@angular/router'
+import { NavigationCancel, NavigationEnd, Router } from '@angular/router'
import { ServerService } from '../server'
@Injectable()
// 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
}
}
}
+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
<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>
<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>
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',
@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
})
}
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`)
+ })
}
}
</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>
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'
rel: 'self',
type: 'application/activity+json',
href: actor.url
+ },
+ {
+ rel: 'http://ostatus.org/schema/1.0/subscribe',
+ template: WEBSERVER.URL + '/remote-interaction?uri={uri}'
}
]
}
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 () {