diff --git a/lib/default.php b/lib/default.php
index 18e26ef1e9..1de401d090 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -355,6 +355,7 @@ $default =
'Nodeinfo' => [],
'OpenID' => [],
'OpportunisticQM' => [],
+ 'RemoteFollow' => [],
'ActivityPub' => [], // The order is important here (IT HAS TO COME BEFORE OSTATUS)
'OStatus' => [],
'Poll' => [],
diff --git a/plugins/RemoteFollow/EVENTS.txt b/plugins/RemoteFollow/EVENTS.txt
new file mode 100644
index 0000000000..64771aa0fa
--- /dev/null
+++ b/plugins/RemoteFollow/EVENTS.txt
@@ -0,0 +1,8 @@
+RemoteFollowConnectProfile: when the plugin is "one url away" from redirecting to the remote follower instance; federation plugins must find and retrieve the url for redirection
+- User $target local user to be followed
+- string $profile remote follower's ID
+- string|null &$url url for redirection
+
+RemoteFollowPullProfile: when the plugin needs to pull the remote profile from its uri; federation plugins must pull, store and retrieve the profile's local object
+- string $uri the remote profile's uri
+- null|Profile &$profile pulled profile
\ No newline at end of file
diff --git a/plugins/RemoteFollow/README.md b/plugins/RemoteFollow/README.md
new file mode 100644
index 0000000000..d0992dc7c6
--- /dev/null
+++ b/plugins/RemoteFollow/README.md
@@ -0,0 +1,41 @@
+# RemoteFollow Plugin
+(c) 2019 Free Software Foundation, Inc
+
+This is the README file for GNU social's ActivityPub plugin.
+It includes general information about the plugin.
+
+## About
+
+This plugin adds remote-follow button support to GNU social.
+
+## Installation
+
+This plugin is enabled by default.
+
+## Settings
+
+This plugin has no settings.
+
+## License
+
+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, in the file "COPYING". If not, see
+.
+
+ IMPORTANT NOTE: The GNU Affero General Public License (AGPL) has
+ *different requirements* from the "regular" GPL. In particular, if
+ you make modifications to the plugin source code on your server,
+ you *MUST MAKE AVAILABLE* the modified version of the source code
+ to your users under the same license. This is a legal requirement
+ of using the software, and if you do not wish to share your
+ modifications, *YOU MAY NOT USE THIS PLUGIN*.
diff --git a/plugins/RemoteFollow/RemoteFollowPlugin.php b/plugins/RemoteFollow/RemoteFollowPlugin.php
new file mode 100644
index 0000000000..81f1f14d43
--- /dev/null
+++ b/plugins/RemoteFollow/RemoteFollowPlugin.php
@@ -0,0 +1,159 @@
+.
+
+/**
+ * Remote Follow implementation for GNU social
+ *
+ * @package GNUsocial
+ * @author Bruno Casteleiro
+ * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
+ * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
+ */
+
+defined('GNUSOCIAL') || die();
+
+class RemoteFollowPlugin extends Plugin
+{
+ const PLUGIN_VERSION = '0.1.0';
+
+ /**
+ * Route/Reroute urls
+ *
+ * @param URLMapper $m
+ * @return void
+ */
+ public function onRouterInitialized(URLMapper $m): void
+ {
+ // discovery
+ $m->connect('main/remotefollow/nickname/:nickname',
+ ['action' => 'RemoteFollowInit'],
+ ['nickname' => Nickname::DISPLAY_FMT]);
+ $m->connect('main/remotefollow',
+ ['action' => 'RemoteFollowInit']);
+
+ // remote follow
+ $m->connect('main/remotefollowsub',
+ ['action' => 'RemoteFollowSub']);
+ }
+
+ /**
+ * Add remote-follow button to someone's profile
+ *
+ * @param HTMLOutputter $out
+ * @param Profile $target
+ * @return bool hook return value
+ */
+ public function onStartProfileRemoteSubscribe(HTMLOutputter $out, Profile $target): bool
+ {
+ if (common_logged_in() || !$target->isLocal()) {
+ return true;
+ }
+
+ $out->elementStart('li', 'entity_subscribe');
+ $url = common_local_url('RemoteFollowInit', ['nickname' => $target->getNickname()]);
+ $out->element('a',
+ ['href' => $url,
+ 'class' => 'entity_remote_subscribe'],
+ // TRANS: Link text for the follow button
+ _m('Subscribe'));
+ $out->elementEnd('li');
+
+ return true;
+ }
+
+ /**
+ * Add remote-follow button in the subscriptions list
+ *
+ * @param Action $action
+ * @return bool hook return value
+ */
+ public function onStartShowSubscriptionsContent(Action $action): bool
+ {
+ $this->showEntityRemoteSubscribe($action);
+ return true;
+ }
+
+ /**
+ * Add remote-follow button to the profile subscriptions minilist
+ *
+ * @param Action $action
+ * @return bool hook return value
+ */
+ public function onEndShowSubscriptionsMiniList(Action $action): bool
+ {
+ $this->showEntityRemoteSubscribe($action);
+ return true;
+ }
+
+ /**
+ * Add webfinger profile link for remote subscription
+ */
+ function onEndWebFingerProfileLinks(XML_XRD $xrd, Profile $target): bool
+ {
+ $xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe',
+ common_local_url('RemoteFollowSub') . '?profile={uri}',
+ null, // type not set
+ true); // isTemplate
+
+ return true;
+ }
+
+ /**
+ * Plugin version information
+ *
+ * @param array $versions
+ * @return bool hook return value
+ */
+ public function onPluginVersion(array &$versions): bool
+ {
+ $versions[] = [
+ 'name' => 'RemoteFollow',
+ 'version' => self::PLUGIN_VERSION,
+ 'author' => 'Bruno Casteleiro',
+ 'homepage' => 'https://notabug.org/diogo/gnu-social/src/nightly/plugins/RemoteFollow',
+ // TRANS: Plugin description.
+ 'rawdescription' => _m('Add remote-follow button support to GNU social')
+ ];
+ return true;
+ }
+
+ /**
+ * Add remote-follow button to some required action
+ *
+ * @param Action $action
+ * @return void
+ */
+ public function showEntityRemoteSubscribe(Action $action): void
+ {
+ if (!$action->getScoped() instanceof Profile) {
+ // not logged in
+ return;
+ }
+
+ if ($action->getScoped()->sameAs($action->getTarget())) {
+ $action->elementStart('div', 'entity_actions');
+ $action->elementStart('p', ['id' => 'entity_remote_subscribe',
+ 'class' => 'entity_subscribe']);
+ $action->element('a',
+ ['href' => common_local_url('RemoteFollowSub'),
+ 'class' => 'entity_remote_subscribe'],
+ // TRANS: Link text for link to remote subscribe.
+ _m('Remote'));
+ $action->elementEnd('p');
+ $action->elementEnd('div');
+ }
+ }
+}
diff --git a/plugins/RemoteFollow/actions/remotefollowinit.php b/plugins/RemoteFollow/actions/remotefollowinit.php
new file mode 100644
index 0000000000..4b025b0442
--- /dev/null
+++ b/plugins/RemoteFollow/actions/remotefollowinit.php
@@ -0,0 +1,236 @@
+.
+
+/**
+ * Remote Follow implementation for GNU social
+ *
+ * @package GNUsocial
+ * @author Bruno Casteleiro
+ * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
+ * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
+ */
+
+defined('GNUSOCIAL') || die();
+
+/**
+ * Remote-follow preparation action
+ *
+ * @category Plugin
+ * @package GNUsocial
+ * @author Bruno Casteleiro
+ * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
+ */
+class RemoteFollowInitAction extends Action
+{
+ protected $target = null;
+ protected $profile = null;
+
+ protected function prepare(array $args = [])
+ {
+ parent::prepare($args);
+
+ if (common_logged_in()) {
+ // TRANS: Client error displayed when the user is logged in.
+ $this->clientError(_m('You can use the local subscription!'));
+ }
+
+ // Local user the remote wants to follow
+ $nickname = $this->trimmed('nickname');
+
+ $this->target = User::getKV('nickname', $nickname);
+ if (!$this->target instanceof User) {
+ // TRANS: Client error displayed when targeting an invalid user.
+ $this->clientError(_m('No such user.'));
+ }
+
+ // Webfinger or profile URL of the remote user
+ $this->profile = $this->trimmed('profile');
+
+ return true;
+ }
+
+ protected function handle()
+ {
+ parent::handle();
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ /* Use a session token for CSRF protection. */
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ // TRANS: Error displayed when the session token does not match or is not given.
+ $this->showForm(_m('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ $url = null;
+ if (Event::handle('RemoteFollowConnectProfile', [$this->target, $this->profile, &$url])) {
+ // use ported ostatus connect functions to find remote url
+ $url = self::ostatusConnect($this->target, $this->profile);
+ }
+
+ if (!is_null($url)) {
+ common_redirect($url, 303);
+ }
+
+ // TRANS: Error displayed when there is failure in connecting with the remote profile.
+ $this->showForm(_m('There was a problem connecting with the remote profile. '.
+ 'Try again, please.'));
+
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function showContent()
+ {
+ // TRANS: Form legend. %s is a nickname.
+ $header = sprintf(_m('Subscribe to %s'), $this->target->getNickname());
+ // TRANS: Button text to subscribe to a profile.
+ $submit = _m('BUTTON', 'Subscribe');
+
+ $this->elementStart('form',
+ ['id' => 'form_ostatus_connect',
+ 'method' => 'post',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('RemoteFollowInit')]);
+ $this->elementStart('fieldset');
+ $this->element('legend', null, $header);
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li', ['id' => 'ostatus_nickname']);
+
+ $this->input('nickname',
+ // TRANS: Field label.
+ _m('User nickname'),
+ $this->target->getNickname(),
+ // TRANS: Field title.
+ _m('Nickname of the user you want to follow.'));
+
+ $this->elementEnd('li');
+ $this->elementStart('li', ['id' => 'ostatus_profile']);
+ $this->input('profile',
+ // TRANS: Field label.
+ _m('Profile Account'),
+ $this->profile,
+ // TRANS: Tooltip for field label "Profile Account".
+ _m('Your account ID (e.g. user@example.com).'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('submit', $submit);
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ public function showForm($err = null)
+ {
+ if ($err) {
+ $this->error = $err;
+ }
+
+ if ($this->boolean('ajax')) {
+ $this->startHTML('text/xml;charset=utf-8');
+ $this->elementStart('head');
+ // TRANS: Form title.
+ $this->element('title', null, _m('TITLE','Subscribe to user'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showContent();
+ $this->elementEnd('body');
+ $this->endHTML();
+ } else {
+ $this->showPage();
+ }
+ }
+
+ /**
+ * Find remote url to finish follow interaction
+ *
+ * @param User $target local user to be followed
+ * @param string $remote ID of the remote subscriber
+ * @return string|null
+ */
+ public static function ostatusConnect(User $target, string $remote): ?string
+ {
+ $validate = new Validate();
+ $opts = ['allowed_schemes' => ['http', 'https', 'acct']];
+ if ($validate->uri($remote, $opts)) {
+ $bits = parse_url($remote);
+ if ($bits['scheme'] == 'acct') {
+ return self::connectWebfinger($bits['path'], $target);
+ } else {
+ return self::connectProfile($remote, $target);
+ }
+ } else if (strpos($remote, '@') !== false) {
+ return self::connectWebfinger($remote, $target);
+ }
+
+ common_log(LOG_ERR, 'Must provide a remote profile');
+ return null;
+ }
+
+ /**
+ * Find remote url to finish follow interaction from a webfinger ID
+ *
+ * @param string $acct
+ * @param User $target
+ * @return string|null
+ * @see ostatusConnect
+ */
+ public static function connectWebfinger(string $acct, User $target): ?string
+ {
+ $target = common_local_url('userbyid', ['id' => $target->getID()]);
+
+ $disco = new Discovery;
+ $xrd = $disco->lookup($acct);
+
+ $link = $xrd->get('http://ostatus.org/schema/1.0/subscribe');
+ if (!is_null($link)) {
+ // We found a URL - let's redirect!
+ if (!empty($link->template)) {
+ $url = Discovery::applyTemplate($link->template, $target);
+ } else {
+ $url = $link->href;
+ }
+ common_log(LOG_INFO, "Retrieving url $url for remote subscriber $acct");
+ return $url;
+ }
+
+ common_log(LOG_ERR, "Could not confirm remote profile $acct");
+ return null;
+ }
+
+ /**
+ * Find remote url to finish follow interaction from an url ID
+ *
+ * @param string $acct
+ * @param User $target
+ * @return string
+ * @see ostatusConnect
+ */
+ public static function connectProfile(string $url, User $target): string
+ {
+ $target = common_local_url('userbyid', ['id' => $target->getID()]);
+
+ // @fixme hack hack! We should look up the remote sub URL from XRDS
+ $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $url);
+ $suburl .= '?profile=' . urlencode($target);
+
+ common_log(LOG_INFO, "Retrieving url $suburl for remote subscriber $url");
+ return $suburl;
+ }
+}
\ No newline at end of file
diff --git a/plugins/RemoteFollow/actions/remotefollowsub.php b/plugins/RemoteFollow/actions/remotefollowsub.php
new file mode 100644
index 0000000000..a713542e52
--- /dev/null
+++ b/plugins/RemoteFollow/actions/remotefollowsub.php
@@ -0,0 +1,394 @@
+.
+
+/**
+ * Remote Follow implementation for GNU social
+ *
+ * @package GNUsocial
+ * @author Bruno Casteleiro
+ * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
+ * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
+ */
+
+defined('GNUSOCIAL') || die();
+
+/**
+ * Remote-follow follow action
+ *
+ * @category Plugin
+ * @package GNUsocial
+ * @author Bruno Casteleiro
+ * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
+ */
+class RemoteFollowSubAction extends Action
+{
+ protected $uri; // acct: or uri of remote entity
+ protected $profile; // profile of remote entity, if valid
+
+ protected function prepare(array $args = [])
+ {
+ parent::prepare($args);
+
+ if (!common_logged_in()) {
+ common_set_returnto($_SERVER['REQUEST_URI']);
+ if (Event::handle('RedirectToLogin', [$this, null])) {
+ common_redirect(common_local_url('login'), 303);
+ }
+ return false;
+ }
+
+ if (!$this->profile && $this->arg('profile')) {
+ $this->uri = $this->trimmed('profile');
+
+ $profile = null;
+ if (!Event::handle('RemoteFollowPullProfile', [$this->uri, &$profile]) && !is_null($profile)) {
+ $this->profile = $profile;
+ } else {
+ // TRANS: Error displayed when there's failure in fetching the remote profile.
+ $this->error = _m('Sorry, we could not reach that address. ' .
+ 'Please make sure it is a valid address and try again later.');
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Handles the submission.
+ *
+ * @return void
+ */
+ protected function handle(): void
+ {
+ parent::handle();
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost();
+ } else {
+ $this->showForm();
+ }
+ }
+
+ /**
+ * Show the initial form, when we haven't yet been given a valid
+ * remote profile.
+ *
+ * @return void
+ */
+ public function showInputForm(): void
+ {
+ $this->elementStart('form', ['method' => 'post',
+ 'id' => 'form_ostatus_sub',
+ 'class' => 'form_settings',
+ 'action' => $this->selfLink()]);
+
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset', ['id' => 'settings_feeds']);
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('profile',
+ // TRANS: Field label for a field that takes an user address.
+ _m('Subscribe to'),
+ $this->uri,
+ // TRANS: Tooltip for field label "Subscribe to".
+ _m('User\'s address, like nickname@example.com or http://example.net/nickname.'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ // TRANS: Button text.
+ $this->submit('validate', _m('BUTTON','Continue'));
+
+ $this->elementEnd('fieldset');
+
+ $this->elementEnd('form');
+ }
+
+ /**
+ * Show the preview-and-confirm form. We've got a valid remote
+ * profile and are ready to poke it!
+ *
+ * @return void
+ */
+ public function showPreviewForm(): void
+ {
+ if (!$this->preview()) {
+ return;
+ }
+
+ $this->elementStart('div', 'entity_actions');
+ $this->elementStart('ul');
+ $this->elementStart('li', 'entity_subscribe');
+ $this->elementStart('form', ['method' => 'post',
+ 'id' => 'form_ostatus_sub',
+ 'class' => 'form_remote_authorize',
+ 'action' => $this->selfLink()]);
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+ $this->hidden('profile', $this->uri);
+ $this->submit('submit',
+ // TRANS: Button text.
+ _m('BUTTON','Confirm'),
+ 'submit',
+ null,
+ // TRANS: Tooltip for button "Confirm".
+ _m('Subscribe to this user'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->elementEnd('div');
+ }
+
+ /**
+ * Show a preview for a remote user's profile.
+ *
+ * @return bool true if we're ok to try subscribing, false otherwise
+ */
+ public function preview(): bool
+ {
+ if ($this->scoped->isSubscribed($this->profile)) {
+ $this->element('div',
+ ['class' => 'error'],
+ // TRANS: Extra paragraph in remote profile view when already subscribed.
+ _m('You are already subscribed to this user.'));
+ $ok = false;
+ } else {
+ $ok = true;
+ }
+
+ $avatarUrl = $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
+
+ $this->showEntity($this->profile,
+ $this->profile->getUrl(),
+ $avatarUrl,
+ $this->profile->getDescription());
+ return $ok;
+ }
+
+ /**
+ * Show someone's profile.
+ *
+ * @return void
+ */
+ public function showEntity(Profile $entity, string $profile_url, string $avatar, ?string $note): void
+ {
+ $nickname = $entity->getNickname();
+ $fullname = $entity->getFullname();
+ $homepage = $entity->getHomepage();
+ $location = $entity->getLocation();
+
+ $this->elementStart('div', 'entity_profile vcard');
+ $this->element('img', ['src' => $avatar,
+ 'class' => 'photo avatar entity_depiction',
+ 'width' => AVATAR_PROFILE_SIZE,
+ 'height' => AVATAR_PROFILE_SIZE,
+ 'alt' => $nickname]);
+
+ $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname entity_nickname';
+ $this->elementStart('a', ['href' => $profile_url,
+ 'class' => 'url '.$hasFN]);
+ $this->text($nickname);
+ $this->elementEnd('a');
+
+ if (!is_null($fullname)) {
+ $this->elementStart('div', 'fn entity_fn');
+ $this->text($fullname);
+ $this->elementEnd('div');
+ }
+
+ if (!is_null($location)) {
+ $this->elementStart('div', 'label entity_location');
+ $this->text($location);
+ $this->elementEnd('div');
+ }
+
+ if (!is_null($homepage)) {
+ $this->elementStart('a', ['href' => $homepage,
+ 'class' => 'url entity_url']);
+ $this->text($homepage);
+ $this->elementEnd('a');
+ }
+
+ if (!is_null($note)) {
+ $this->elementStart('div', 'note entity_note');
+ $this->text($note);
+ $this->elementEnd('div');
+ }
+ $this->elementEnd('div');
+ }
+
+ /**
+ * Redirect on successful remote follow
+ *
+ * @return void
+ */
+ public function success(): void
+ {
+ $url = common_local_url('subscriptions', ['nickname' => $this->scoped->getNickname()]);
+ common_redirect($url, 303);
+ }
+
+ /**
+ * Attempt to finalize subscription.
+ *
+ * @return void
+ */
+ public function follow(): void
+ {
+ if ($this->scoped->isSubscribed($this->profile)) {
+ // TRANS: Remote subscription dialog error.
+ $this->showForm(_m('Already subscribed!'));
+ } elseif (Subscription::start($this->scoped, $this->profile)) {
+ $this->success();
+ } else {
+ // TRANS: Remote subscription dialog error.
+ $this->showForm(_m('Remote subscription failed!'));
+ }
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * @return void
+ */
+ public function handlePost(): void
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ // TRANS: Client error displayed when the session token does not match or is not given.
+ $this->showForm(_m('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->profile && $this->arg('submit')) {
+ $this->follow();
+ return;
+ }
+
+ $this->showForm();
+ }
+
+ /**
+ * Show the appropriate form based on our input state.
+ *
+ * @return void
+ */
+ public function showForm(?string $err = null): void
+ {
+ if ($err) {
+ $this->error = $err;
+ }
+
+ if ($this->boolean('ajax')) {
+ $this->startHTML('text/xml;charset=utf-8');
+ $this->elementStart('head');
+ // TRANS: Form title.
+ $this->element('title', null, _m('Subscribe to user'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showContent();
+ $this->elementEnd('body');
+ $this->endHTML();
+ } else {
+ $this->showPage();
+ }
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string title of the page
+ */
+ public function title(): string
+ {
+ // TRANS: Page title for remote subscription form.
+ return !empty($this->uri) ? _m('Confirm') : _m('Remote subscription');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return string instructions for use
+ */
+ public function getInstructions(): string
+ {
+ // TRANS: Instructions.
+ return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
+ }
+
+ /**
+ * Show page notice.
+ *
+ * @return void
+ */
+ public function showPageNotice(): void
+ {
+ if (!empty($this->error)) {
+ $this->element('p', 'error', $this->error);
+ }
+ }
+
+ /**
+ * Content area of the page
+ *
+ * @return void
+ */
+ public function showContent(): void
+ {
+ if ($this->profile) {
+ $this->showPreviewForm();
+ } else {
+ $this->showInputForm();
+ }
+ }
+
+ /**
+ * Show javascript headers
+ *
+ * @return void
+ */
+ public function showScripts(): void
+ {
+ parent::showScripts();
+ $this->autofocus('profile');
+ }
+
+ /**
+ * Return url for this action
+ *
+ * @return string
+ */
+ function selfLink(): string
+ {
+ return common_local_url('RemoteFollowSub');
+ }
+
+ /**
+ * Disable the send-notice form at the top of the page.
+ * This is really just a hack for the broken CSS in the Cloudy theme,
+ * I think; copying from other non-notice-navigation pages that do this
+ * as well. There will be plenty of others also broken.
+ *
+ * @fixme fix the cloudy theme
+ * @fixme do this in a more general way
+ */
+ public function showNoticeForm(): void
+ {
+ // nop
+ }
+}
\ No newline at end of file
diff --git a/plugins/RemoteFollow/locale/RemoteFollow.pot b/plugins/RemoteFollow/locale/RemoteFollow.pot
new file mode 100644
index 0000000000..298a3e8035
--- /dev/null
+++ b/plugins/RemoteFollow/locale/RemoteFollow.pot
@@ -0,0 +1,163 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-08-25 01:23+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. TRANS: Link text for the follow button
+#: RemoteFollowPlugin.php:70
+msgid "Subscribe"
+msgstr ""
+
+#. TRANS: Plugin description.
+#: RemoteFollowPlugin.php:127
+msgid "Add remote-follow button support to GNU social"
+msgstr ""
+
+#. TRANS: Link text for link to remote subscribe.
+#: RemoteFollowPlugin:153
+msgid "Remote"
+msgstr ""
+
+#. TRANS: Client error displayed when the user is logged in.
+#: remotefollowinit.php:46
+msgid "You can use the local subscription!"
+msgstr ""
+
+#. TRANS: Client error displayed when targeting an invalid user.
+#: remotefollowinit.php:55
+msgid "No such user."
+msgstr ""
+
+#. TRANS: Error displayed when the session token does not match or is not given.
+#: remotefollowinit.php:73, remotefollowsub.php:272
+msgid ""
+"There was a problem with your session token. "
+"Try again, please."
+msgstr ""
+
+#. TRANS: Error displayed when there is failure in connecting with the remote profile.
+#: remotefollowinit.php:89
+msgid ""
+"There was a problem connecting with the remote profile. "
+"Try again, please."
+msgstr ""
+
+#. TRANS: Form legend. %s is a nickname.
+#: remotefollowinit.php:100
+msgid "Subscribe to %s"
+msgstr ""
+
+#. TRANS: Button text to subscribe to a profile.
+#: remotefollowinit.php:102
+msgctxt "BUTTON"
+msgid "Subscribe"
+msgstr ""
+
+#. TRANS: Field label.
+#: remotefollowinit.php:118
+msgid "User nickname"
+msgstr ""
+
+#. TRANS: Field title.
+#: remotefollowinit.php:121
+msgid "Nickname of the user you want to follow."
+msgstr ""
+
+#. TRANS: Field label.
+#: remotefollowinit.php:127
+msgid "Profile Account"
+msgstr ""
+
+#. TRANS: Tooltip for field label "Profile Account".
+#: remotefollowinit.php:130
+msgid "Your account ID (e.g. user@example.com)."
+msgstr ""
+
+#. TRANS: Form title.
+#: remotefollowinit.php:148
+msgctxt "TITLE"
+msgid "Subscribe to user"
+msgstr ""
+
+#. TRANS: Error displayed when there's failure in fetching the remote profile.
+#: remotefollowsub.php:60
+msgid ""
+"Sorry, we could not reach that address. "
+"Please make sure it is a valid address and try again later."
+msgstr ""
+
+#. TRANS: Field label for a field that takes an user address.
+#: remotefollowsub.php:104
+msgid "Subscribe to"
+msgstr ""
+
+#. TRANS: Tooltip for field label "Subscribe to".
+#: remotefollowsub.php:107
+msgid "User\'s address, like nickname@example.com or http://example.net/nickname."
+msgstr ""
+
+#. TRANS: Button text.
+#: remotefollowsub.php:111
+msgctxt "BUTTON"
+msgid "Continue"
+msgstr ""
+
+#. TRANS: Button text.
+#: remotefollowsub.php:142
+msgctxt "BUTTON"
+msgid "Confirm"
+msgstr ""
+
+#. TRANS: Tooltip for button "Confirm".
+#: remotefollowsub.php:147
+msgid "Subscribe to this user"
+msgstr ""
+
+#. TRANS: Extra paragraph in remote profile view when already subscribed.
+#: remotefollowsub.php:165
+msgid "You are already subscribed to this user."
+msgstr ""
+
+#. TRANS: Remote subscription dialog error.
+#: remotefollowsub.php:252
+msgid "Already subscribed!"
+msgstr ""
+
+#. TRANS: Remote subscription dialog error.
+#: remotefollowsub.php:257
+msgid "Remote subscription failed!"
+msgstr ""
+
+#. TRANS: Form title.
+#: remotefollowsub.php:300
+msgid "Subscribe to user"
+msgstr ""
+
+#. TRANS: Page title for remote subscription form.
+#: remotefollowsub.php:319
+msgid "Confirm"
+msgstr ""
+
+#. TRANS: Page title for remote subscription form.
+#: remotefollowsub.php:319
+msgid "Remote subscription"
+msgstr ""
+
+#. TRANS: Instructions.
+#: remotefollowsub.php:330
+msgid "You can subscribe to users from other supported sites. Paste their address or profile URI below"
+msgstr ""
\ No newline at end of file
diff --git a/plugins/RemoteFollow/locale/en_GB/LC_MESSAGES/RemoteFollow.po b/plugins/RemoteFollow/locale/en_GB/LC_MESSAGES/RemoteFollow.po
new file mode 100644
index 0000000000..9521d4d91a
--- /dev/null
+++ b/plugins/RemoteFollow/locale/en_GB/LC_MESSAGES/RemoteFollow.po
@@ -0,0 +1,170 @@
+# Translation file for GNU social - the free software social networking platform
+# Copyright (C) 2019 Free Software Foundation, Inc http://www.fsf.org
+# This file is under https://www.gnu.org/licenses/agpl v3 or later
+#
+# Translators:
+# Bruno Casteleiro , 2019
+msgid ""
+msgstr ""
+"Project-Id-Version: GNU social\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-08-25 01:23+0100\n"
+"PO-Revision-Date: 2019-08-25 01:23+0100\n"
+"Last-Translator: Bruno Casteleiro \n"
+"Language-Team: English (United Kingdom) (http://www.transifex.com/gnu-social/gnu-social/language/en_GB/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en_GB\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. TRANS: Link text for the follow button
+#: RemoteFollowPlugin.php:70
+msgid "Subscribe"
+msgstr "Subscribe"
+
+#. TRANS: Plugin description.
+#: RemoteFollowPlugin.php:127
+msgid "Add remote-follow button support to GNU social"
+msgstr "Add remote-follow button support to GNU social"
+
+#. TRANS: Link text for link to remote subscribe.
+#: RemoteFollowPlugin:153
+msgid "Remote"
+msgstr "Remote"
+
+#. TRANS: Client error displayed when the user is logged in.
+#: remotefollowinit.php:46
+msgid "You can use the local subscription!"
+msgstr "You can use the local subscription!"
+
+#. TRANS: Client error displayed when targeting an invalid user.
+#: remotefollowinit.php:55
+msgid "No such user."
+msgstr "No such user."
+
+#. TRANS: Error displayed when the session token does not match or is not given.
+#: remotefollowinit.php:73, remotefollowsub.php:272
+msgid ""
+"There was a problem with your session token. "
+"Try again, please."
+msgstr ""
+"There was a problem with your session token. "
+"Try again, please."
+
+#. TRANS: Error displayed when there is failure in connecting with the remote profile.
+#: remotefollowinit.php:89
+msgid ""
+"There was a problem connecting with the remote profile. "
+"Try again, please."
+msgstr ""
+"There was a problem connecting with the remote profile. "
+"Try again, please."
+
+#. TRANS: Form legend. %s is a nickname.
+#: remotefollowinit.php:100
+msgid "Subscribe to %s"
+msgstr "Subscribe to %s"
+
+#. TRANS: Button text to subscribe to a profile.
+#: remotefollowinit.php:102
+msgctxt "BUTTON"
+msgid "Subscribe"
+msgstr "Subscribe"
+
+#. TRANS: Field label.
+#: remotefollowinit.php:118
+msgid "User nickname"
+msgstr "User nickname"
+
+#. TRANS: Field title.
+#: remotefollowinit.php:121
+msgid "Nickname of the user you want to follow."
+msgstr "Nickname of the user you want to follow."
+
+#. TRANS: Field label.
+#: remotefollowinit.php:127
+msgid "Profile Account"
+msgstr "Profile Account"
+
+#. TRANS: Tooltip for field label "Profile Account".
+#: remotefollowinit.php:130
+msgid "Your account ID (e.g. user@example.com)."
+msgstr "Your account ID (e.g. user@example.com)."
+
+#. TRANS: Form title.
+#: remotefollowinit.php:148
+msgctxt "TITLE"
+msgid "Subscribe to user"
+msgstr "Subscribe to user"
+
+#. TRANS: Error displayed when there's failure in fetching the remote profile.
+#: remotefollowsub.php:60
+msgid ""
+"Sorry, we could not reach that address. "
+"Please make sure it is a valid address and try again later."
+msgstr ""
+"Sorry, we could not reach that address. "
+"Please make sure it is a valid address and try again later."
+
+#. TRANS: Field label for a field that takes an user address.
+#: remotefollowsub.php:104
+msgid "Subscribe to"
+msgstr "Subscribe to"
+
+#. TRANS: Tooltip for field label "Subscribe to".
+#: remotefollowsub.php:107
+msgid "User's address, like nickname@example.com or http://example.net/nickname."
+msgstr "User's address, like nickname@example.com or http://example.net/nickname."
+
+#. TRANS: Button text.
+#: remotefollowsub.php:111
+msgctxt "BUTTON"
+msgid "Continue"
+msgstr "Continue"
+
+#. TRANS: Button text.
+#: remotefollowsub.php:142
+msgctxt "BUTTON"
+msgid "Confirm"
+msgstr "Confirm"
+
+#. TRANS: Tooltip for button "Confirm".
+#: remotefollowsub.php:147
+msgid "Subscribe to this user"
+msgstr "Subscribe to this user"
+
+#. TRANS: Extra paragraph in remote profile view when already subscribed.
+#: remotefollowsub.php:165
+msgid "You are already subscribed to this user."
+msgstr "You are already subscribed to this user."
+
+#. TRANS: Remote subscription dialog error.
+#: remotefollowsub.php:252
+msgid "Already subscribed!"
+msgstr "Already subscribed!"
+
+#. TRANS: Remote subscription dialog error.
+#: remotefollowsub.php:257
+msgid "Remote subscription failed!"
+msgstr "Remote subscription failed!"
+
+#. TRANS: Form title.
+#: remotefollowsub.php:300
+msgid "Subscribe to user"
+msgstr "Subscribe to user"
+
+#. TRANS: Page title for remote subscription form.
+#: remotefollowsub.php:319
+msgid "Confirm"
+msgstr "Confirm"
+
+#. TRANS: Page title for remote subscription form.
+#: remotefollowsub.php:319
+msgid "Remote subscription"
+msgstr "Remote subscription"
+
+#. TRANS: Instructions.
+#: remotefollowsub.php:330
+msgid "You can subscribe to users from other supported sites. Paste their address or profile URI below"
+msgstr "You can subscribe to users from other supported sites. Paste their address or profile URI below"
\ No newline at end of file
diff --git a/plugins/RemoteFollow/locale/pt/LC_MESSAGES/RemoteFollow.po b/plugins/RemoteFollow/locale/pt/LC_MESSAGES/RemoteFollow.po
new file mode 100644
index 0000000000..9b64cd4461
--- /dev/null
+++ b/plugins/RemoteFollow/locale/pt/LC_MESSAGES/RemoteFollow.po
@@ -0,0 +1,170 @@
+# Translation file for GNU social - the free software social networking platform
+# Copyright (C) 2019 Free Software Foundation, Inc http://www.fsf.org
+# This file is under https://www.gnu.org/licenses/agpl v3 or later
+#
+# Translators:
+# Bruno Casteleiro , 2019
+msgid ""
+msgstr ""
+"Project-Id-Version: GNU social\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-08-25 01:23+0100\n"
+"PO-Revision-Date: 2019-08-25 01:23+0100\n"
+"Last-Translator: Bruno Casteleiro \n"
+"Language-Team: Portuguese (http://www.transifex.com/gnu-social/gnu-social/language/pt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. TRANS: Link text for the follow button
+#: RemoteFollowPlugin.php:70
+msgid "Subscribe"
+msgstr "Subscrever"
+
+#. TRANS: Plugin description.
+#: RemoteFollowPlugin.php:127
+msgid "Add remote-follow button support to GNU social"
+msgstr "Adiciona suporte para o botão de subscrição remota ao GNU social"
+
+#. TRANS: Link text for link to remote subscribe.
+#: RemoteFollowPlugin:153
+msgid "Remote"
+msgstr "Remoto"
+
+#. TRANS: Client error displayed when the user is logged in.
+#: remotefollowinit.php:46
+msgid "You can use the local subscription!"
+msgstr "Pode usar a subscrição local!"
+
+#. TRANS: Client error displayed when targeting an invalid user.
+#: remotefollowinit.php:55
+msgid "No such user."
+msgstr "Utilizador inexistente."
+
+#. TRANS: Error displayed when the session token does not match or is not given.
+#: remotefollowinit.php:73, remotefollowsub.php:272
+msgid ""
+"There was a problem with your session token. "
+"Try again, please."
+msgstr ""
+"Ocorreu um problema com o seu token de sessão. "
+"Tente outra vez, por favor."
+
+#. TRANS: Error displayed when there is failure in connecting with the remote profile.
+#: remotefollowinit.php:89
+msgid ""
+"There was a problem connecting with the remote profile. "
+"Try again, please."
+msgstr ""
+"Ocorreu um problema ao conectar com o perfil remoto. "
+"Tente outra vez, por favor."
+
+#. TRANS: Form legend. %s is a nickname.
+#: remotefollowinit.php:100
+msgid "Subscribe to %s"
+msgstr "Subscrever %s"
+
+#. TRANS: Button text to subscribe to a profile.
+#: remotefollowinit.php:102
+msgctxt "BUTTON"
+msgid "Subscribe"
+msgstr "Subscrever"
+
+#. TRANS: Field label.
+#: remotefollowinit.php:118
+msgid "User nickname"
+msgstr "Apelido do utilizador"
+
+#. TRANS: Field title.
+#: remotefollowinit.php:121
+msgid "Nickname of the user you want to follow."
+msgstr "Apelido do utilizador que pretende seguir."
+
+#. TRANS: Field label.
+#: remotefollowinit.php:127
+msgid "Profile Account"
+msgstr "Conta do perfil"
+
+#. TRANS: Tooltip for field label "Profile Account".
+#: remotefollowinit.php:130
+msgid "Your account ID (e.g. user@example.com)."
+msgstr "O ID da sua conta (e.g. utilizador@exemplo.com)."
+
+#. TRANS: Form title.
+#: remotefollowinit.php:148
+msgctxt "TITLE"
+msgid "Subscribe to user"
+msgstr "Subscrever utilizador"
+
+#. TRANS: Error displayed when there's failure in fetching the remote profile.
+#: remotefollowsub.php:60
+msgid ""
+"Sorry, we could not reach that address. "
+"Please make sure it is a valid address and try again later."
+msgstr ""
+"Desculpe, não conseguimos aceder a esse endereço. "
+"Por favor verifique que é um endereço válido e tente outra vez mais tarde."
+
+#. TRANS: Field label for a field that takes an user address.
+#: remotefollowsub.php:104
+msgid "Subscribe to"
+msgstr "Subscrever"
+
+#. TRANS: Tooltip for field label "Subscribe to".
+#: remotefollowsub.php:107
+msgid "User's address, like nickname@example.com or http://example.net/nickname."
+msgstr "Endereço do utilizador, do tipo apelido@exemplo.com ou http://exemplo.net/apelido."
+
+#. TRANS: Button text.
+#: remotefollowsub.php:111
+msgctxt "BUTTON"
+msgid "Continue"
+msgstr "Continuar"
+
+#. TRANS: Button text.
+#: remotefollowsub.php:142
+msgctxt "BUTTON"
+msgid "Confirm"
+msgstr "Confirmar"
+
+#. TRANS: Tooltip for button "Confirm".
+#: remotefollowsub.php:147
+msgid "Subscribe to this user"
+msgstr "Subscrever este utilizador"
+
+#. TRANS: Extra paragraph in remote profile view when already subscribed.
+#: remotefollowsub.php:165
+msgid "You are already subscribed to this user."
+msgstr "Este utilizador ja foi subscrito."
+
+#. TRANS: Remote subscription dialog error.
+#: remotefollowsub.php:252
+msgid "Already subscribed!"
+msgstr "Já subscrito!"
+
+#. TRANS: Remote subscription dialog error.
+#: remotefollowsub.php:257
+msgid "Remote subscription failed!"
+msgstr "Subscrição remota falhou!"
+
+#. TRANS: Form title.
+#: remotefollowsub.php:300
+msgid "Subscribe to user"
+msgstr "Subscrever utilizador"
+
+#. TRANS: Page title for remote subscription form.
+#: remotefollowsub.php:319
+msgid "Confirm"
+msgstr "Confirmar"
+
+#. TRANS: Page title for remote subscription form.
+#: remotefollowsub.php:319
+msgid "Remote subscription"
+msgstr "Subscrição remota"
+
+#. TRANS: Instructions.
+#: remotefollowsub.php:330
+msgid "You can subscribe to users from other supported sites. Paste their address or profile URI below"
+msgstr "Pode subscrever utilizadores de outros sites suportados. Coloque o endereço do mesmo em baixo"
\ No newline at end of file