Merge pull request #314 from nextcloud/feature/268/ostatus-follow

Add the ability to follow from external website
pull/412/head^2
Julius Härtl 2019-02-20 21:01:32 +01:00 zatwierdzone przez GitHub
commit a4a5b29550
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 438 dodań i 11 usunięć

Wyświetl plik

@ -54,6 +54,10 @@ return [
['name' => 'ActivityPub#followers', 'url' => '/@{username}/followers', 'verb' => 'GET'],
['name' => 'ActivityPub#following', 'url' => '/@{username}/following', 'verb' => 'GET'],
['name' => 'OStatus#subscribe', 'url' => '/ostatus/follow/{uri}', 'verb' => 'GET'],
['name' => 'OStatus#followRemote', 'url' => '/api/v1/ostatus/followRemote/{local}', 'verb' => 'GET'],
['name' => 'OStatus#getLink', 'url' => '/api/v1/ostatus/link/{local}/{account}', 'verb' => 'GET'],
['name' => 'SocialPub#displayPost', 'url' => '/@{username}/{postId}', 'verb' => 'GET'],
['name' => 'Local#streamHome', 'url' => '/api/v1/stream/home', 'verb' => 'GET'],

Wyświetl plik

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
/**
* Nextcloud - Social Support
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Maxence Lange <maxence@artificial-owl.com>
* @copyright 2018, Maxence Lange <maxence@artificial-owl.com>
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Social\Controller;
use daita\MySmallPhpTools\Exceptions\ArrayNotFoundException;
use daita\MySmallPhpTools\Traits\Nextcloud\TNCDataResponse;
use daita\MySmallPhpTools\Traits\TArrayTools;
use Exception;
use OCA\Social\AppInfo\Application;
use OCA\Social\Exceptions\RetrieveAccountFormatException;
use OCA\Social\Service\AccountService;
use OCA\Social\Service\CacheActorService;
use OCA\Social\Service\CurlService;
use OCA\Social\Service\MiscService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
class OStatusController extends Controller {
use TNCDataResponse;
use TArrayTools;
/** @var CacheActorService */
private $cacheActorService;
/** @var AccountService */
private $accountService;
/** @var CurlService */
private $curlService;
/** @var MiscService */
private $miscService;
/** @var IUserManager */
private $userSession;
/**
* OStatusController constructor.
*
* @param IRequest $request
* @param CacheActorService $cacheActorService
* @param AccountService $accountService
* @param CurlService $curlService
* @param MiscService $miscService
*/
public function __construct(
IRequest $request, CacheActorService $cacheActorService, AccountService $accountService,
CurlService $curlService, MiscService $miscService, IUserSession $userSession
) {
parent::__construct(Application::APP_NAME, $request);
$this->cacheActorService = $cacheActorService;
$this->accountService = $accountService;
$this->curlService = $curlService;
$this->miscService = $miscService;
$this->userSession = $userSession;
}
/**
* @NoCSRFRequired
* @NoAdminRequired
*
* @param string $uri
*
* @return Response
*/
public function subscribe(string $uri): Response {
try {
$actor = $this->cacheActorService->getFromAccount($uri);
$user = $this->userSession->getUser();
if ($user === null) {
return $this->fail('Failed to retrieve current user');
}
return new TemplateResponse('social', 'ostatus', [
'serverData' => [
'account' => $actor->getAccount(),
'currentUser' => [
'uid' => $user->getUID(),
'displayName' => $user->getDisplayName(),
]
]
], 'guest');
} catch (Exception $e) {
return $this->fail($e);
}
}
/**
* @NoCSRFRequired
* @NoAdminRequired
* @PublicPage
*
* @param string $local
* @return Response
*/
public function followRemote(string $local): Response {
try {
$following = $this->accountService->getActor($local);
return new TemplateResponse('social', 'ostatus', [
'serverData' => [
'local' => $local,
'account' => $following->getAccount()
]
], 'guest');
} catch (\Exception $e) {
return $this->fail($e);
}
}
/**
* @NoCSRFRequired
* @NoAdminRequired
* @PublicPage
*
* @param string $local
* @param string $account
*
* @return Response
*/
public function getLink(string $local, string $account): Response {
try {
$following = $this->accountService->getActor($local);
$result = $this->curlService->webfingerAccount($account);
try {
$link = $this->extractArray(
'rel', 'http://ostatus.org/schema/1.0/subscribe',
$this->getArray('links', $result)
);
} catch (ArrayNotFoundException $e) {
throw new RetrieveAccountFormatException();
}
$template = $this->get('template', $link, '');
$url = str_replace('{uri}', $following->getAccount(), $template);
return $this->success(['url' => $url]);
} catch (Exception $e) {
return $this->fail($e);
}
}
}

Wyświetl plik

@ -161,6 +161,7 @@ class AccountService {
* @throws NoUserException
* @throws SocialAppConfigException
* @throws UrlCloudException
* @throws ItemUnknownException
*/
public function getActorFromUserId(string $userId, bool $create = false): Person {
$this->miscService->confirmUserId($userId);

Wyświetl plik

@ -90,21 +90,15 @@ class CurlService {
/**
* @param string $account
*
* @return Person
* @throws InvalidOriginException
* @return array
* @throws InvalidResourceException
* @throws MalformedArrayException
* @throws RedundancyLimitException
* @throws RequestContentException
* @throws RetrieveAccountFormatException
* @throws RequestNetworkException
* @throws RequestResultSizeException
* @throws RequestServerException
* @throws SocialAppConfigException
* @throws ItemUnknownException
* @throws RequestResultNotJsonException
*/
public function retrieveAccount(string $account): Person {
public function webfingerAccount(string $account): array {
$account = $this->withoutBeginAt($account);
// we consider an account is like an email
@ -122,6 +116,29 @@ class CurlService {
$request->setAddress($host);
$result = $this->request($request);
return $result;
}
/**
* @param string $account
*
* @return Person
* @throws InvalidOriginException
* @throws InvalidResourceException
* @throws MalformedArrayException
* @throws RedundancyLimitException
* @throws RequestContentException
* @throws RetrieveAccountFormatException
* @throws RequestNetworkException
* @throws RequestResultSizeException
* @throws RequestServerException
* @throws SocialAppConfigException
* @throws ItemUnknownException
*/
public function retrieveAccount(string $account): Person {
$result = $this->webfingerAccount($account);
try {
$link = $this->extractArray('rel', 'self', $this->getArray('links', $result));
} catch (ArrayNotFoundException $e) {

Wyświetl plik

@ -83,6 +83,11 @@ $finger = [
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $href
],
[
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
'template' => urldecode(
$href = $urlGenerator->linkToRouteAbsolute('social.OStatus.subscribe', ['uri' => '{uri}']))
]
]
];

Wyświetl plik

@ -35,6 +35,9 @@
</a>
</p>
<follow-button :account="accountInfo.account" />
<button v-if="serverData.public" class="primary" @click="followRemote">
{{ t('social', 'Follow') }}
</button>
</div>
<!-- TODO: we have no details, timeline and follower list for non-local accounts for now -->
<ul v-if="accountInfo.details && accountInfo.local" class="user-profile--sections">
@ -145,7 +148,9 @@ export default {
}
},
methods: {
followRemote() {
window.open(OC.generateUrl('/apps/social/api/v1/ostatus/followRemote/' + encodeURI(this.uid)), 'followRemote', 'width=433,height=600toolbar=no,menubar=no,scrollbars=yes,resizable=yes')
}
}
}

41
src/ostatus.js 100644
Wyświetl plik

@ -0,0 +1,41 @@
/*
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import store from './store'
import OStatus from './views/OStatus'
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken)
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('social', 'js/')
Vue.prototype.t = t
Vue.prototype.n = n
Vue.prototype.OC = OC
Vue.prototype.OCA = OCA
/* eslint-disable-next-line no-new */
new Vue({
render: h => h(OStatus),
store: store
}).$mount('#vue-content')

Wyświetl plik

@ -0,0 +1,135 @@
<template>
<div v-if="accountInfo">
<div v-if="!serverData.local">
<h2>{{ t('social', 'Follow on Nextcloud Social') }}</h2>
<p>{{ t('social', 'Hello') }} <avatar :user="currentUser.uid" :size="16" />{{ currentUser.displayName }}</p>
<p v-if="!isFollowing">
{{ t('social', 'Please confirm that you want to follow this account:') }}
</p>
<avatar :url="avatarUrl" :disable-tooltip="true" :size="128" />
<h2>{{ displayName }}</h2>
<form v-if="!isFollowing" @submit.prevent="follow">
<input type="submit" class="primary" value="Follow">
</form>
<p v-else>
<span class="icon icon-checkmark-white" />
{{ t('social', 'You are following this account') }}
</p>
<div v-if="isFollowing">
<button @click="close">
{{ t('social', 'Close') }}
</button>
</div>
</div>
<div v-if="serverData.local">
<p>{{ t('social', 'You are going to follow:') }}</p>
<avatar :user="serverData.local" :disable-tooltip="true" :size="128" />
<h2>{{ displayName }}</h2>
<form @submit.prevent="followRemote">
<input v-model="remote" type="text" :placeholder="t('social', 'name@domain of your federation account')">
<input type="submit" class="primary" :value="t('social', 'Continue')">
</form>
<p>{{ t('social', 'This step is needed as the user is probably not registered on the same server as you are. We will redirect you to your homeserver to follow this account.') }}</p>
</div>
</div>
<div v-else :class="{ 'icon-loading-dark': !accountInfo }" />
</template>
<style scoped>
h2, p {
color: var(--color-primary-text);
}
p .icon {
display: inline-block;
}
.avatardiv {
vertical-align: -4px;
margin-right: 3px;
filter: drop-shadow(0 0 0.5rem #333);
margin-top: 10px;
margin-bottom: 20px;
}
</style>
<style>
.wrapper {
margin-top: 20px;
}
</style>
<script>
import { Avatar } from 'nextcloud-vue'
import axios from 'nextcloud-axios'
import currentuserMixin from './../mixins/currentUserMixin'
export default {
name: 'App',
components: {
Avatar
},
mixins: [currentuserMixin],
data() {
return {
remote: ''
}
},
computed: {
isFollowing() {
return this.$store.getters.isFollowingUser(this.account)
},
account() {
return this.serverData.account
},
avatarUrl() {
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.accountInfo.id)
},
accountInfo: function() {
return this.$store.getters.getAccount(this.serverData.account)
},
currentUser() {
return window.oc_current_user
},
displayName() {
if (typeof this.accountInfo.name !== 'undefined' && this.accountInfo.name !== '') {
return this.accountInfo.name
}
return this.account
}
},
beforeMount: function() {
// importing server data into the store
const serverDataElmt = document.getElementById('serverData')
if (serverDataElmt !== null) {
const serverData = JSON.parse(document.getElementById('serverData').dataset.server)
if (serverData.currentUser) {
window.oc_current_user = JSON.parse(JSON.stringify(serverData.currentUser))
}
this.$store.commit('setServerData', serverData)
if (this.serverData.account && !this.serverData.local) {
this.$store.dispatch('fetchAccountInfo', this.serverData.account)
}
if (this.serverData.local) {
this.$store.dispatch('fetchPublicAccountInfo', this.serverData.local)
}
}
},
methods: {
follow() {
this.$store.dispatch('followAccount', { currentAccount: this.cloudId, accountToFollow: this.account }).then(() => {
})
},
followRemote() {
axios.get(OC.generateUrl(`/apps/social/api/v1/ostatus/link/${this.serverData.local}/` + encodeURI(this.remote))).then((a) => {
window.location = a.data.result.url
})
},
close() {
window.close()
}
}
}
</script>

Wyświetl plik

@ -0,0 +1,28 @@
<?php
/**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
script('social', 'ostatus');
style('social', 'style');
?>
<span id="serverData" data-server="<?php p(json_encode($_['serverData']));?>"></span>
<div id="vue-content"></div>

Wyświetl plik

@ -3,11 +3,14 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
entry: path.join(__dirname, 'src', 'main.js'),
entry: {
social: path.join(__dirname, 'src', 'main.js'),
ostatus: path.join(__dirname, 'src', 'ostatus.js'),
},
output: {
path: path.resolve(__dirname, './js'),
publicPath: '/js/',
filename: 'social.js',
filename: '[name].js',
chunkFilename: '[name].[chunkhash].js'
},
module: {