From 72fae04e9705bf14181095efc4cd23dec883681a Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sat, 2 Oct 2021 11:44:47 -0400 Subject: [PATCH] Add revoke follow feature - 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 | 28 ++++++ doc/de/Addons.md | 5 + src/Core/Protocol.php | 72 +++++++++++++- src/Model/Contact.php | 32 +++++- src/Module/Contact.php | 10 ++ src/Module/Contact/Revoke.php | 108 +++++++++++++++++++++ src/Protocol/ActivityPub/Transmitter.php | 9 +- static/routes.config.php | 1 + view/templates/contact_edit.tpl | 11 ++- view/theme/frio/templates/contact_edit.tpl | 1 + view/theme/vier/templates/contact_edit.tpl | 11 ++- 11 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 src/Module/Contact/Revoke.php diff --git a/doc/Addons.md b/doc/Addons.md index a478a176e..89c3c3d99 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -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); diff --git a/doc/de/Addons.md b/doc/de/Addons.md index 34e4a5358..e7fecc29a 100644 --- a/doc/de/Addons.md +++ b/doc/de/Addons.md @@ -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); diff --git a/src/Core/Protocol.php b/src/Core/Protocol.php index 7972bf4a3..bce6bc699 100644 --- a/src/Core/Protocol.php +++ b/src/Core/Protocol.php @@ -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']; + } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 5bb0608fd..2e7d4b68b 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -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']) { diff --git a/src/Module/Contact.php b/src/Module/Contact.php index b671de5e1..a01a2c785 100644 --- a/src/Module/Contact.php +++ b/src/Module/Contact.php @@ -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 index 000000000..e9b5a4424 --- /dev/null +++ b/src/Module/Contact/Revoke.php @@ -0,0 +1,108 @@ +. + * + */ + +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'), + ]); + } +} diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index d9b6eddb6..d84baa555 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -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); } /** diff --git a/static/routes.config.php b/static/routes.config.php index f47051e32..c19392a09 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -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]], diff --git a/view/templates/contact_edit.tpl b/view/templates/contact_edit.tpl index f59787782..05779f3a4 100644 --- a/view/templates/contact_edit.tpl +++ b/view/templates/contact_edit.tpl @@ -15,13 +15,14 @@ {{$contact_action_button}} diff --git a/view/theme/frio/templates/contact_edit.tpl b/view/theme/frio/templates/contact_edit.tpl index 7f07a6e41..aa9bedb87 100644 --- a/view/theme/frio/templates/contact_edit.tpl +++ b/view/theme/frio/templates/contact_edit.tpl @@ -27,6 +27,7 @@ {{/if}}
  • {{$contact_actions.block.label}}
  • {{$contact_actions.ignore.label}}
  • + {{if $contact_actions.revoke_follow.url}}
  • {{/if}} {{if $contact_actions.delete.url}}
  • {{/if}} diff --git a/view/theme/vier/templates/contact_edit.tpl b/view/theme/vier/templates/contact_edit.tpl index 7341d81d8..44506d99b 100644 --- a/view/theme/vier/templates/contact_edit.tpl +++ b/view/theme/vier/templates/contact_edit.tpl @@ -16,13 +16,14 @@ {{$contact_action_button}}