]> git.mxchange.org Git - friendica.git/commitdiff
Add revoke follow feature
authorHypolite Petovan <hypolite@mrpetovan.com>
Sat, 2 Oct 2021 15:44:47 +0000 (11:44 -0400)
committerHypolite Petovan <hypolite@mrpetovan.com>
Sat, 2 Oct 2021 18:01:46 +0000 (14:01 -0400)
- Add new follow revoke module
- Add new hooks: revoke_follow, support_follow, support_revoke_follow
- Add link in contact page action menu

doc/Addons.md
doc/de/Addons.md
src/Core/Protocol.php
src/Model/Contact.php
src/Module/Contact.php
src/Module/Contact/Revoke.php [new file with mode: 0644]
src/Protocol/ActivityPub/Transmitter.php
static/routes.config.php
view/templates/contact_edit.tpl
view/theme/frio/templates/contact_edit.tpl
view/theme/vier/templates/contact_edit.tpl

index a478a176ec40c3969de9def531a9eeb9fdf074a6..89c3c3d99ac65ecdece6221a0af514a73817ac4f 100644 (file)
@@ -479,6 +479,22 @@ Hook data:
 - **uid** (input): the user to return the contact data for (can be empty for public contacts).
 - **result** (output): Set by the hook function to indicate a successful detection.
 
+### support_follow
+
+Called to assert whether a connector addon provides follow capabilities.
+
+Hook data:
+- **protocol** (input): shorthand for the protocol. List of values is available in `src/Core/Protocol.php`.
+- **result** (output): should be true if the connector provides follow capabilities, left alone otherwise.
+
+### support_revoke_follow
+
+Called to assert whether a connector addon provides follow revocation capabilities.
+
+Hook data:
+- **protocol** (input): shorthand for the protocol. List of values is available in `src/Core/Protocol.php`.
+- **result** (output): should be true if the connector provides follow revocation capabilities, left alone otherwise.
+
 ### follow
 
 Called before adding a new contact for a user to handle non-native network remote contact (like Twitter).
@@ -497,6 +513,14 @@ Hook data:
 - **two_way** (input): wether to stop sharing with the remote contact as well.
 - **result** (output): wether the unfollowing is successful or not.
 
+### revoke_follow
+
+Called when making a remote contact on a non-native network (like Twitter) unfollow you.
+
+Hook data:
+- **contact** (input): the remote contact (uid = local revoking user id) array.
+- **result** (output): a boolean value indicating wether the operation was successful or not.
+
 ## Complete list of hook callbacks
 
 Here is a complete list of all hook callbacks with file locations (as of 24-Sep-2018). Please see the source for details of any hooks not documented above.
@@ -751,7 +775,11 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
 
 ### src/Core/Protocol.php
 
+    Hook::callAll('support_follow', $hook_data);
+    Hook::callAll('support_revoke_follow', $hook_data);
     Hook::callAll('unfollow', $hook_data);
+    Kook::callAll('revoke_follow', $hook_data);
+
 ### src/Core/StorageManager
 
     Hook::callAll('storage_instance', $data);
index 34e4a53586cc523e165e859c976ebb2300cd1185..e7fecc29a510f1fb50d8a2f6feac0eb6b473e8e6 100644 (file)
@@ -414,7 +414,12 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
     Hook::callAll('logged_in', $a->user);
 
 ### src/Core/Protocol.php
+
+    Hook::callAll('support_follow', $hook_data);
+    Hook::callAll('support_revoke_follow', $hook_data);
     Hook::callAll('unfollow', $hook_data);
+    Kook::callAll('revoke_follow', $hook_data);
+
 ### src/Core/StorageManager
 
     Hook::callAll('storage_instance', $data);
index 7972bf4a3db8255e3552d7ba74107568e16603fa..bce6bc699cc8cd31b9966f99faed31e7798736c8 100644 (file)
@@ -71,6 +71,44 @@ class Protocol
 
        const PHANTOM   = 'unkn';    // Place holder
 
+       /**
+        * Returns whether the provided protocol supports following
+        *
+        * @param $protocol
+        * @return bool
+        * @throws HTTPException\InternalServerErrorException
+        */
+       public static function supportsFollow($protocol): bool
+       {
+               if (in_array($protocol, self::NATIVE_SUPPORT)) {
+                       return true;
+               }
+
+               $result = null;
+               Hook::callAll('support_follow', $result);
+
+               return $result === true;
+       }
+
+       /**
+        * Returns whether the provided protocol supports revoking inbound follows
+        *
+        * @param $protocol
+        * @return bool
+        * @throws HTTPException\InternalServerErrorException
+        */
+       public static function supportsRevokeFollow($protocol): bool
+       {
+               if (in_array($protocol, self::NATIVE_SUPPORT)) {
+                       return true;
+               }
+
+               $result = null;
+               Hook::callAll('support_revoke_follow', $result);
+
+               return $result === true;
+       }
+
        /**
         * Returns the address string for the provided profile URL
         *
@@ -212,7 +250,7 @@ class Protocol
                        return ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $user['uid']);
                }
 
-               // Catch-all addon hook
+               // Catch-all hook for connector addons
                $hook_data = [
                        'contact' => $contact,
                        'two_way' => $two_way,
@@ -222,4 +260,36 @@ class Protocol
 
                return $hook_data['result'];
        }
+
+       /**
+        * Revoke an incoming follow from the provided contact
+        *
+        * @param array $contact Private contact (uid != 0) array
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        * @throws \ImagickException
+        */
+       public static function revokeFollow(array $contact)
+       {
+               if (empty($contact['network'])) {
+                       throw new \InvalidArgumentException('Missing network key in contact array');
+               }
+
+               $protocol = $contact['network'];
+               if ($protocol == Protocol::DFRN && !empty($contact['protocol'])) {
+                       $protocol = $contact['protocol'];
+               }
+
+               if ($protocol == Protocol::ACTIVITYPUB) {
+                       return ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $contact['uid']);
+               }
+
+               // Catch-all hook for connector addons
+               $hook_data = [
+                       'contact' => $contact,
+                       'result' => null,
+               ];
+               Hook::callAll('revoke_follow', $hook_data);
+
+               return $hook_data['result'];
+       }
 }
index 5bb0608fd687bc2931c869d804fe2d057a3c2dd9..2e7d4b68b586332b0a31e1a29f1278aa5b3a36c0 100644 (file)
@@ -849,6 +849,36 @@ class Contact
                return $result;
        }
 
+       /**
+        * Revoke follow privileges of the remote user contact
+        *
+        * @param array   $contact  Contact unfriended
+        * @return bool|null Whether the remote operation is successful or null if no remote operation was performed
+        * @throws HTTPException\InternalServerErrorException
+        * @throws \ImagickException
+        */
+       public static function revokeFollow(array $contact): bool
+       {
+               if (empty($contact['network'])) {
+                       throw new \InvalidArgumentException('Empty network in contact array');
+               }
+
+               if (empty($contact['uid'])) {
+                       throw new \InvalidArgumentException('Unexpected public contact record');
+               }
+
+               $result = Protocol::revokeFollow($contact);
+
+               // A null value here means the remote network doesn't support explicit follow revocation, we can still
+               // break the locally recorded relationship
+               if ($result !== false) {
+                       DBA::update('contact', ['rel' => $contact['rel'] == self::FRIEND ? self::SHARING : self::NOTHING], ['id' => $contact['id']]);
+               }
+
+               return $result;
+       }
+
+
        /**
         * Marks a contact for archival after a communication issue delay
         *
@@ -1022,7 +1052,7 @@ class Contact
 
                $follow_link = '';
                $unfollow_link = '';
-               if (!$contact['self'] && in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
+               if (!$contact['self'] && Protocol::supportsFollow($contact['network'])) {
                        if ($contact['uid'] && in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
                                $unfollow_link = 'unfollow?url=' . urlencode($contact['url']) . '&auto=1';
                        } elseif(!$contact['pending']) {
index b671de5e1938d7134e6c8849d9bae6162da1d52e..a01a2c78568c1ef5643e28897b4cf4587242cd5d 100644 (file)
@@ -1148,6 +1148,16 @@ class Contact extends BaseModule
                ];
 
                if ($contact['uid'] != 0) {
+                       if (Protocol::supportsRevokeFollow($contact['network']) && in_array($contact['rel'], [Model\Contact::FOLLOWER, Model\Contact::FRIEND])) {
+                               $contact_actions['revoke_follow'] = [
+                                       'label' => DI::l10n()->t('Revoke Follow'),
+                                       'url'   => 'contact/' . $contact['id'] . '/revoke',
+                                       'title' => DI::l10n()->t('Revoke the follow from this contact'),
+                                       'sel'   => '',
+                                       'id'    => 'revoke_follow',
+                               ];
+                       }
+
                        $contact_actions['delete'] = [
                                'label' => DI::l10n()->t('Delete'),
                                'url'   => 'contact/' . $contact['id'] . '/drop?t=' . $formSecurityToken,
diff --git a/src/Module/Contact/Revoke.php b/src/Module/Contact/Revoke.php
new file mode 100644 (file)
index 0000000..e9b5a44
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @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 <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Module\Contact;
+
+use Friendica\BaseModule;
+use Friendica\Content\Nav;
+use Friendica\Core\Protocol;
+use Friendica\Core\Renderer;
+use Friendica\Database\DBA;
+use Friendica\DI;
+use Friendica\Model;
+use Friendica\Module\Contact;
+use Friendica\Module\Security\Login;
+use Friendica\Network\HTTPException;
+
+class Revoke extends BaseModule
+{
+       /** @var array */
+       private static $contact;
+
+       public static function init(array $parameters = [])
+       {
+               if (!local_user()) {
+                       return;
+               }
+
+               $data = Model\Contact::getPublicAndUserContactID($parameters['id'], local_user());
+               if (!DBA::isResult($data)) {
+                       throw new HTTPException\NotFoundException(DI::l10n()->t('Unknown contact.'));
+               }
+
+               if (empty($data['user'])) {
+                       throw new HTTPException\ForbiddenException();
+               }
+
+               self::$contact = Model\Contact::getById($data['user']);
+
+               if (self::$contact['deleted']) {
+                       throw new HTTPException\NotFoundException(DI::l10n()->t('Contact is deleted.'));
+               }
+
+               if (!empty(self::$contact['network']) && self::$contact['network'] == Protocol::PHANTOM) {
+                       throw new HTTPException\NotFoundException(DI::l10n()->t('Contact is being deleted.'));
+               }
+       }
+
+       public static function post(array $parameters = [])
+       {
+               if (!local_user()) {
+                       throw new HTTPException\UnauthorizedException();
+               }
+
+               self::checkFormSecurityTokenRedirectOnError('contact/' . $parameters['id'], 'contact_revoke');
+
+               $result = Model\Contact::revokeFollow(self::$contact);
+               if ($result === true) {
+                       notice(DI::l10n()->t('Follow was successfully revoked.'));
+               } elseif ($result === null) {
+                       notice(DI::l10n()->t('Follow was successfully revoked, however the remote contact won\'t be aware of this revokation.'));
+               } else {
+                       notice(DI::l10n()->t('Unable to revoke follow, please try again later or contact the administrator.'));
+               }
+
+               DI::baseUrl()->redirect('contact/' . $parameters['id']);
+       }
+
+       public static function content(array $parameters = []): string
+       {
+               if (!local_user()) {
+                       return Login::form($_SERVER['REQUEST_URI']);
+               }
+
+               Nav::setSelected('contact');
+
+               return Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_drop_confirm.tpl'), [
+                       '$l10n' => [
+                               'header'  => DI::l10n()->t('Revoke Follow'),
+                               'message' => DI::l10n()->t('Do you really want to revoke this contact\'s follow? This cannot be undone and they will have to manually follow you back again.'),
+                               'confirm' => DI::l10n()->t('Yes'),
+                               'cancel'  => DI::l10n()->t('Cancel'),
+                       ],
+                       '$contact'       => Contact::getContactTemplateVars(self::$contact),
+                       '$method'        => 'post',
+                       '$confirm_url'   => DI::args()->getCommand(),
+                       '$confirm_name'  => 'form_security_token',
+                       '$confirm_value' => BaseModule::getFormSecurityToken('contact_revoke'),
+               ]);
+       }
+}
index d9b6eddb6a83d1140c41a17757a65f5fd0846179..d84baa55588e55bdaff1a26a3803eefca112c179 100644 (file)
@@ -2047,15 +2047,16 @@ class Transmitter
         * @param string  $target Target profile
         * @param         $id
         * @param integer $uid    User ID
-        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        * @return bool Operation success
+        * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function sendContactReject($target, $id, $uid)
+       public static function sendContactReject($target, $id, $uid): bool
        {
                $profile = APContact::getByURL($target);
                if (empty($profile['inbox'])) {
                        Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]);
-                       return;
+                       return false;
                }
 
                $owner = User::getOwnerDataById($uid);
@@ -2075,7 +2076,7 @@ class Transmitter
                Logger::debug('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id);
 
                $signed = LDSignature::sign($data, $owner);
-               HTTPSignature::transmit($signed, $profile['inbox'], $uid);
+               return HTTPSignature::transmit($signed, $profile['inbox'], $uid);
        }
 
        /**
index f47051e321e3ffa12e135b9998cb5aa646a8f928..c19392a09376507346a98b118e7cf0204f601a82 100644 (file)
@@ -239,6 +239,7 @@ return [
                '/{id:\d+}/ignore'            => [Module\Contact::class,           [R::GET]],
                '/{id:\d+}/poke'              => [Module\Contact\Poke::class,      [R::GET, R::POST]],
                '/{id:\d+}/posts'             => [Module\Contact::class,           [R::GET]],
+               '/{id:\d+}/revoke'            => [Module\Contact\Revoke::class,    [R::GET, R::POST]],
                '/{id:\d+}/update'            => [Module\Contact::class,           [R::GET]],
                '/{id:\d+}/updateprofile'     => [Module\Contact::class,           [R::GET]],
                '/archived'                   => [Module\Contact::class,           [R::GET]],
index f59787782645479b0e682a460d0df4afc371071c..05779f3a46dbbe3323044b7c46457014e88ec8b6 100644 (file)
                                                <a class="btn" rel="#contact-actions-menu" href="#" id="contact-edit-actions-button">{{$contact_action_button}}</a>
 
                                                <ul role="menu" aria-haspopup="true" id="contact-actions-menu" class="menu-popup">
-                                                       {{if $lblsuggest}}<li role="menuitem"><a  href="#" title="{{$contact_actions.suggest.title}}" onclick="window.location.href='{{$contact_actions.suggest.url}}'; return false;">{{$contact_actions.suggest.label}}</a></li>{{/if}}
-                                                       {{if $poll_enabled}}<li role="menuitem"><a  href="#" title="{{$contact_actions.update.title}}" onclick="window.location.href='{{$contact_actions.update.url}}'; return false;">{{$contact_actions.update.label}}</a></li>{{/if}}
+                                                       {{if $lblsuggest}}<li role="menuitem"><a href="#" title="{{$contact_actions.suggest.title}}" onclick="window.location.href='{{$contact_actions.suggest.url}}'; return false;">{{$contact_actions.suggest.label}}</a></li>{{/if}}
+                                                       {{if $poll_enabled}}<li role="menuitem"><a href="#" title="{{$contact_actions.update.title}}" onclick="window.location.href='{{$contact_actions.update.url}}'; return false;">{{$contact_actions.update.label}}</a></li>{{/if}}
                                                        {{if $contact_actions.updateprofile}}<li role="menuitem"><a href="{{$contact_actions.updateprofile.url}}" title="{{$contact_actions.updateprofile.title}}">{{$contact_actions.updateprofile.label}}</a></li>{{/if}}
                                                        <li class="divider"></li>
-                                                       <li role="menuitem"><a  href="#" title="{{$contact_actions.block.title}}" onclick="window.location.href='{{$contact_actions.block.url}}'; return false;">{{$contact_actions.block.label}}</a></li>
-                                                       <li role="menuitem"><a  href="#" title="{{$contact_actions.ignore.title}}" onclick="window.location.href='{{$contact_actions.ignore.url}}'; return false;">{{$contact_actions.ignore.label}}</a></li>
-                                                       {{if $contact_actions.delete.url}}<li role="menuitem"><a  href="{{$contact_actions.delete.url}}" title="{{$contact_actions.delete.title}}" onclick="return confirmDelete();">{{$contact_actions.delete.label}}</a></li> {{/if}}
+                                                       <li role="menuitem"><a href="#" title="{{$contact_actions.block.title}}" onclick="window.location.href='{{$contact_actions.block.url}}'; return false;">{{$contact_actions.block.label}}</a></li>
+                                                       <li role="menuitem"><a href="#" title="{{$contact_actions.ignore.title}}" onclick="window.location.href='{{$contact_actions.ignore.url}}'; return false;">{{$contact_actions.ignore.label}}</a></li>
+                                                       {{if $contact_actions.revoke_follow.url}}<li role="menuitem"><a href="{{$contact_actions.revoke_follow.url}}" title="{{$contact_actions.revoke_follow.title}}">{{$contact_actions.revoke_follow.label}}</a></li>{{/if}}
+                                                       {{if $contact_actions.delete.url}}<li role="menuitem"><a href="{{$contact_actions.delete.url}}" title="{{$contact_actions.delete.title}}" onclick="return confirmDelete();">{{$contact_actions.delete.label}}</a></li> {{/if}}
                                                </ul>
                                        </div>
 
index 7f07a6e415c6a423a3c4ef985d029d6714d8a280..aa9bedb872faf3014ce0de6f7d3704560008d80c 100644 (file)
@@ -27,6 +27,7 @@
                                                        {{/if}}
                                                        <li role="presentation"><a role="menuitem" href="{{$contact_actions.block.url}}" title="{{$contact_actions.block.title}}">{{$contact_actions.block.label}}</a></li>
                                                        <li role="presentation"><a role="menuitem" href="{{$contact_actions.ignore.url}}" title="{{$contact_actions.ignore.title}}">{{$contact_actions.ignore.label}}</a></li>
+                            {{if $contact_actions.revoke_follow.url}}<li role="presentation"><button role="menuitem" type="button" class="btn-link" title="{{$contact_actions.revoke_follow.title}}" onclick="addToModal('{{$contact_actions.revoke_follow.url}}');">{{$contact_actions.revoke_follow.label}}</button></li>{{/if}}
                                                        {{if $contact_actions.delete.url}}<li role="presentation"><button role="menuitem" type="button" class="btn-link" title="{{$contact_actions.delete.title}}" onclick="addToModal('{{$contact_actions.delete.url}}&confirm=1');">{{$contact_actions.delete.label}}</button></li>{{/if}}
                                                </ul>
                                        </li>
index 7341d81d88e7de7063d2194013230dea1a1e7c08..44506d99bb76eedd8dd9330b10348ca0514984d0 100644 (file)
                                                <a class="btn" id="contact-edit-actions-button">{{$contact_action_button}}</a>
 
                                                <ul role="menu" aria-haspopup="true" id="contact-actions-menu" class="menu-popup">
-                                                       {{if $lblsuggest}}<li role="menuitem"><a  href="#" title="{{$contact_actions.suggest.title}}" onclick="window.location.href='{{$contact_actions.suggest.url}}'; return false;">{{$contact_actions.suggest.label}}</a></li>{{/if}}
-                                                       {{if $poll_enabled}}<li role="menuitem"><a  href="#" title="{{$contact_actions.update.title}}" onclick="window.location.href='{{$contact_actions.update.url}}'; return false;">{{$contact_actions.update.label}}</a></li>{{/if}}
+                                                       {{if $lblsuggest}}<li role="menuitem"><a href="#" title="{{$contact_actions.suggest.title}}" onclick="window.location.href='{{$contact_actions.suggest.url}}'; return false;">{{$contact_actions.suggest.label}}</a></li>{{/if}}
+                                                       {{if $poll_enabled}}<li role="menuitem"><a href="#" title="{{$contact_actions.update.title}}" onclick="window.location.href='{{$contact_actions.update.url}}'; return false;">{{$contact_actions.update.label}}</a></li>{{/if}}
                                                        {{if $contact_actions.updateprofile}}<li role="menuitem"><a href="{{$contact_actions.updateprofile.url}}" title="{{$contact_actions.updateprofile.title}}">{{$contact_actions.updateprofile.label}}</a></li>{{/if}}
                                                        <li class="divider"></li>
-                                                       <li role="menuitem"><a  href="#" title="{{$contact_actions.block.title}}" onclick="window.location.href='{{$contact_actions.block.url}}'; return false;">{{$contact_actions.block.label}}</a></li>
-                                                       <li role="menuitem"><a  href="#" title="{{$contact_actions.ignore.title}}" onclick="window.location.href='{{$contact_actions.ignore.url}}'; return false;">{{$contact_actions.ignore.label}}</a></li>
-                                                       {{if $contact_actions.delete.url}}<li role="menuitem"><a  href="{{$contact_actions.delete.url}}" title="{{$contact_actions.delete.title}}" onclick="return confirmDelete();">{{$contact_actions.delete.label}}</a></li>{{/if}}
+                                                       <li role="menuitem"><a href="#" title="{{$contact_actions.block.title}}" onclick="window.location.href='{{$contact_actions.block.url}}'; return false;">{{$contact_actions.block.label}}</a></li>
+                                                       <li role="menuitem"><a href="#" title="{{$contact_actions.ignore.title}}" onclick="window.location.href='{{$contact_actions.ignore.url}}'; return false;">{{$contact_actions.ignore.label}}</a></li>
+                                                       {{if $contact_actions.revoke_follow.url}}<li role="menuitem"><a href="{{$contact_actions.revoke_follow.url}}" title="{{$contact_actions.revoke_follow.title}}">{{$contact_actions.revoke_follow.label}}</a></li>{{/if}}
+                                                       {{if $contact_actions.delete.url}}<li role="menuitem"><a href="{{$contact_actions.delete.url}}" title="{{$contact_actions.delete.title}}" onclick="return confirmDelete();">{{$contact_actions.delete.label}}</a></li>{{/if}}
                                                </ul>
                                        </div>