diff --git a/doc/Addons.md b/doc/Addons.md index 578cffe7c..504ab4398 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -626,7 +626,8 @@ Hook data: Called when unfollowing a remote contact on a non-native network (like Twitter) Hook data: -- **contact** (input): the remote contact (uid = local unfollowing user id) array. +- **contact** (input): the target public contact (uid = 0) array. +- **uid** (input): the id of the source local user. - **result** (output): wether the unfollowing is successful or not. ### revoke_follow diff --git a/mod/unfollow.php b/mod/unfollow.php index 0aa8a87b5..10830bd10 100644 --- a/mod/unfollow.php +++ b/mod/unfollow.php @@ -122,8 +122,7 @@ function unfollow_process(string $url) $owner = User::getOwnerDataById($uid); if (!$owner) { - (new \Friendica\Module\Security\Logout())->init(); - // NOTREACHED + throw new \Friendica\Network\HTTPException\NotFoundException(); } $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", @@ -140,15 +139,10 @@ function unfollow_process(string $url) $return_path = $base_return_path . '/' . $contact['id']; try { - $result = Contact::terminateFriendship($owner, $contact); - - if ($result === false) { - $notice_message = DI::l10n()->t('Unable to unfollow this contact, please retry in a few minutes or contact your administrator.'); - } else { - $notice_message = DI::l10n()->t('Contact was successfully unfollowed'); - } + Contact::unfollow($contact); + $notice_message = DI::l10n()->t('Contact was successfully unfollowed'); } catch (Exception $e) { - DI::logger()->error($e->getMessage(), ['owner' => $owner, 'contact' => $contact]); + DI::logger()->error($e->getMessage(), ['contact' => $contact]); $notice_message = DI::l10n()->t('Unable to unfollow this contact, please contact your administrator'); } diff --git a/src/Console/Contact.php b/src/Console/Contact.php index 11f7f87ce..f051d870a 100644 --- a/src/Console/Contact.php +++ b/src/Console/Contact.php @@ -199,19 +199,18 @@ HELP; throw new RuntimeException('Contact not found'); } - $user = UserModel::getById($contact['uid']); + if (empty($contact['uid'])) { + throw new RuntimeException('Contact must be user-specific (uid != 0)'); + } try { - $result = ContactModel::terminateFriendship($user, $contact); - if ($result === false) { - throw new RuntimeException('Unable to unfollow this contact, please retry in a few minutes or check the logs.'); - } + ContactModel::unfollow($contact); $this->out('Contact was successfully unfollowed'); return true; } catch (\Exception $e) { - DI::logger()->error($e->getMessage(), ['owner' => $user, 'contact' => $contact]); + DI::logger()->error($e->getMessage(), ['contact' => $contact]); throw new RuntimeException('Unable to unfollow this contact, please check the log'); } } diff --git a/src/Core/Protocol.php b/src/Core/Protocol.php index 1b2626525..47d4b4539 100644 --- a/src/Core/Protocol.php +++ b/src/Core/Protocol.php @@ -171,15 +171,15 @@ class Protocol } /** - * Sends an unfriend message. Does not remove the contact + * Sends an unfollow message. Does not remove the contact * - * @param array $user User unfriending - * @param array $contact Contact unfriended + * @param array $contact Target public contact (uid = 0) array + * @param array $user Source local user array * @return bool|null true if successful, false if not, null if no remote action was performed * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function terminateFriendship(array $user, array $contact): ?bool + public static function unfollow(array $contact, array $user): ?bool { if (empty($contact['network'])) { throw new \InvalidArgumentException('Missing network key in contact array'); @@ -216,7 +216,8 @@ class Protocol // Catch-all hook for connector addons $hook_data = [ 'contact' => $contact, - 'result' => null + 'uid' => $user['uid'], + 'result' => null, ]; Hook::callAll('unfollow', $hook_data); diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 9b0020d00..99b1c0971 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -813,26 +813,28 @@ class Contact } /** - * Sends an unfriend message. Removes the contact for two-way unfriending or sharing only protocols (feed an mail) + * Unfollow the remote contact * - * @param array $user User unfriending - * @param array $contact Contact (uid != 0) unfriended - * @param boolean $two_way Revoke eventual inbound follow as well - * @return bool|null true if successful, false if not, null if no remote action was performed + * @param array $contact Target user-specific contact (uid != 0) array * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function terminateFriendship(array $user, array $contact): ?bool + public static function unfollow(array $contact): void { - $result = Protocol::terminateFriendship($user, $contact); - - if ($contact['rel'] == Contact::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { - self::remove($contact['id']); - } else { - self::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); + if (empty($contact['network'])) { + throw new \InvalidArgumentException('Empty network in contact array'); } - return $result; + if (empty($contact['uid'])) { + throw new \InvalidArgumentException('Unexpected public contact record'); + } + + if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) { + $cdata = Contact::getPublicAndUserContactID($contact['id'], $contact['uid']); + Worker::add(PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']); + } + + self::removeSharer($contact); } /** @@ -868,6 +870,36 @@ class Contact return $result; } + /** + * Completely severs a relationship with a contact + * + * @param array $contact User-specific contact (uid != 0) array + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function terminateFriendship(array $contact) + { + if (empty($contact['network'])) { + throw new \InvalidArgumentException('Empty network in contact array'); + } + + if (empty($contact['uid'])) { + throw new \InvalidArgumentException('Unexpected public contact record'); + } + + $cdata = Contact::getPublicAndUserContactID($contact['id'], $contact['uid']); + + if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) { + Worker::add(PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']); + } + + if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) { + Worker::add(PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']); + } + + self::remove($contact['id']); + } + /** * Marks a contact for archival after a communication issue delay @@ -2574,28 +2606,6 @@ class Contact return $result; } - /** - * Unfollow a contact - * - * @param int $cid Public contact id - * @param int $uid User ID - * - * @return bool "true" if unfollowing had been successful - */ - public static function unfollow(int $cid, int $uid) - { - $cdata = self::getPublicAndUserContactID($cid, $uid); - if (empty($cdata['user'])) { - return false; - } - - $contact = self::getById($cdata['user']); - - self::removeSharer([], $contact); - - return true; - } - /** * @param array $importer Owner (local user) data * @param array $contact Existing owner-specific contact data we want to expand the relationship with. Optional. @@ -2755,12 +2765,19 @@ class Contact } } - public static function removeSharer($importer, $contact) + /** + * Update the local relationship when a local user unfollow a contact. + * Removes the contact for sharing-only protocols (feed and mail). + * + * @param array $contact User-specific contact (uid != 0) array + * @throws HTTPException\InternalServerErrorException + */ + public static function removeSharer(array $contact) { - if (($contact['rel'] == self::FRIEND) || ($contact['rel'] == self::FOLLOWER)) { - self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]); - } else { + if ($contact['rel'] == self::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { self::remove($contact['id']); + } else { + self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]); } } diff --git a/src/Module/Api/Mastodon/Accounts/Block.php b/src/Module/Api/Mastodon/Accounts/Block.php index 94c0e3712..41d0a6f5e 100644 --- a/src/Module/Api/Mastodon/Accounts/Block.php +++ b/src/Module/Api/Mastodon/Accounts/Block.php @@ -59,8 +59,7 @@ class Block extends BaseApi Contact\User::setBlocked($cdata['user'], $uid, true); // Mastodon-expected behavior: relationship is severed on block - Contact::terminateFriendship($owner, $contact); - Contact::revokeFollow($contact); + Contact::terminateFriendship($contact); System::jsonExit(DI::mstdnRelationship()->createFromContactId($this->parameters['id'], $uid)->toArray()); } diff --git a/src/Module/Api/Mastodon/Accounts/Unfollow.php b/src/Module/Api/Mastodon/Accounts/Unfollow.php index db1e049db..29aa82b49 100644 --- a/src/Module/Api/Mastodon/Accounts/Unfollow.php +++ b/src/Module/Api/Mastodon/Accounts/Unfollow.php @@ -40,7 +40,14 @@ class Unfollow extends BaseApi DI::mstdnError()->UnprocessableEntity(); } - Contact::unfollow($this->parameters['id'], $uid); + $cdata = Contact::getPublicAndUserContactID($this->parameters['id'], $uid); + if (empty($cdata['user'])) { + DI::mstdnError()->RecordNotFound(); + } + + $contact = Contact::getById($cdata['user']); + + Contact::unfollow($contact); System::jsonExit(DI::mstdnRelationship()->createFromContactId($this->parameters['id'], $uid)->toArray()); } diff --git a/src/Module/Api/Twitter/Friendships/Destroy.php b/src/Module/Api/Twitter/Friendships/Destroy.php index e2e0dd70c..b730f0663 100644 --- a/src/Module/Api/Twitter/Friendships/Destroy.php +++ b/src/Module/Api/Twitter/Friendships/Destroy.php @@ -22,13 +22,18 @@ namespace Friendica\Module\Api\Twitter\Friendships; use Exception; +use Friendica\App; +use Friendica\Core\L10n; use Friendica\Core\Logger; -use Friendica\DI; +use Friendica\Factory\Api\Twitter\User as TwitterUser; use Friendica\Model\Contact; use Friendica\Model\User; +use Friendica\Module\Api\ApiResponse; use Friendica\Module\Api\Twitter\ContactEndpoint; use Friendica\Module\BaseApi; use Friendica\Network\HTTPException; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; /** * Unfollow Contact @@ -37,6 +42,16 @@ use Friendica\Network\HTTPException; */ class Destroy extends ContactEndpoint { + /** @var TwitterUser */ + private $twitterUser; + + public function __construct(App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, TwitterUser $twitterUser, array $server, array $parameters = []) + { + parent::__construct($app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->twitterUser = $twitterUser; + } + protected function post(array $request = []) { BaseApi::checkAllowedScope(BaseApi::SCOPE_WRITE); @@ -66,18 +81,9 @@ class Destroy extends ContactEndpoint $user = $this->twitterUser->createFromContactId($contact_id, $uid, true)->toArray(); try { - $result = Contact::terminateFriendship($owner, $contact); - - if ($result === null) { - Logger::notice(BaseApi::LOG_PREFIX . 'Not supported for {network}', ['module' => 'api', 'action' => 'friendships_destroy', 'network' => $contact['network']]); - throw new HTTPException\ExpectationFailedException('Unfollowing is currently not supported by this contact\'s network.'); - } - - if ($result === false) { - throw new HTTPException\ServiceUnavailableException('Unable to unfollow this contact, please retry in a few minutes or contact your administrator.'); - } + Contact::unfollow($contact); } catch (Exception $e) { - Logger::error(BaseApi::LOG_PREFIX . $e->getMessage(), ['owner' => $owner, 'contact' => $contact]); + Logger::error(BaseApi::LOG_PREFIX . $e->getMessage(), ['contact' => $contact]); throw new HTTPException\InternalServerErrorException('Unable to unfollow this contact, please contact your administrator'); } diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index 50faf987a..2be55b8a5 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -1668,7 +1668,7 @@ class DFRN } if ($activity->match($item["verb"], Activity::UNFRIEND)) { Logger::notice("Lost sharer"); - Contact::removeSharer($importer, $contact, $item); + Contact::removeSharer($contact); return false; } } else { diff --git a/src/Worker/Contact/Unfollow.php b/src/Worker/Contact/Unfollow.php new file mode 100644 index 000000000..a6d8c5944 --- /dev/null +++ b/src/Worker/Contact/Unfollow.php @@ -0,0 +1,57 @@ +. + * + */ + +namespace Friendica\Worker\Contact; + +use Friendica\Core\Protocol; +use Friendica\Core\Worker; +use Friendica\Model\Contact; +use Friendica\Model\User; + +class Unfollow +{ + /** + * Issue asynchronous unfollow message to remote servers. + * The local relationship has already been updated, so we can't use the user-specific contact. + * + * @param int $cid Target public contact (uid = 0) id + * @param int $uid Source local user id + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function execute(int $cid, int $uid) + { + $contact = Contact::getById($cid); + if (empty($contact)) { + return; + } + + $owner = User::getOwnerDataById($uid, false); + if (empty($owner)) { + return; + } + + $result = Protocol::unfollow($contact, $owner); + if ($result === false) { + Worker::defer(); + } + } +} diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index e570b13fe..80628d7db 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -673,7 +673,7 @@ class Notifier } while($contact = DBA::fetch($contacts_stmt)) { - Protocol::terminateFriendship($owner, $contact, true); + Contact::terminateFriendship($contact); } DBA::close($contacts_stmt);