- **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).
- **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.
### 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);
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);
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
*
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,
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'];
+ }
}
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
*
$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']) {
];
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,
--- /dev/null
+<?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'),
+ ]);
+ }
+}
* @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);
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);
}
/**
'/{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]],
<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>
{{/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>
<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>