diff --git a/doc/Addons.md b/doc/Addons.md index 10949c2c4..df3907940 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -494,7 +494,8 @@ 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. -- **dissolve** (input): whether to stop sharing with the remote contact as well. +- **two_way** (input): wether to stop sharing with the remote contact as well. +- **result** (output): wether the unfollowing is successful or not. ## Complete list of hook callbacks diff --git a/include/api.php b/include/api.php index 7656f5029..fe87799cc 100644 --- a/include/api.php +++ b/include/api.php @@ -3778,11 +3778,11 @@ api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', * * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' * @return string|array - * @throws BadRequestException - * @throws ForbiddenException - * @throws ImagickException - * @throws InternalServerErrorException - * @throws NotFoundException + * @throws HTTPException\BadRequestException + * @throws HTTPException\ExpectationFailedException + * @throws HTTPException\ForbiddenException + * @throws HTTPException\InternalServerErrorException + * @throws HTTPException\NotFoundException * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy.html */ function api_friendships_destroy($type) @@ -3790,25 +3790,31 @@ function api_friendships_destroy($type) $uid = api_user(); if ($uid === false) { - throw new ForbiddenException(); + throw new HTTPException\ForbiddenException(); + } + + $owner = User::getOwnerDataById($uid); + if (!$owner) { + Logger::notice(API_LOG_PREFIX . 'No owner {uid} found', ['module' => 'api', 'action' => 'friendships_destroy', 'uid' => $uid]); + throw new HTTPException\NotFoundException('Error Processing Request'); } $contact_id = $_REQUEST['user_id'] ?? 0; if (empty($contact_id)) { Logger::notice(API_LOG_PREFIX . 'No user_id specified', ['module' => 'api', 'action' => 'friendships_destroy']); - throw new BadRequestException("no user_id specified"); + throw new HTTPException\BadRequestException('no user_id specified'); } // Get Contact by given id $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => 0, 'self' => false]); if(!DBA::isResult($contact)) { - Logger::notice(API_LOG_PREFIX . 'No contact found for ID {contact}', ['module' => 'api', 'action' => 'friendships_destroy', 'contact' => $contact_id]); - throw new NotFoundException("no contact found to given ID"); + Logger::notice(API_LOG_PREFIX . 'No public contact found for ID {contact}', ['module' => 'api', 'action' => 'friendships_destroy', 'contact' => $contact_id]); + throw new HTTPException\NotFoundException('no contact found to given ID'); } - $url = $contact["url"]; + $url = $contact['url']; $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", $uid, Contact::SHARING, Contact::FRIEND, Strings::normaliseLink($url), @@ -3817,40 +3823,35 @@ function api_friendships_destroy($type) if (!DBA::isResult($contact)) { Logger::notice(API_LOG_PREFIX . 'Not following contact', ['module' => 'api', 'action' => 'friendships_destroy']); - throw new NotFoundException("Not following Contact"); - } - - if (!in_array($contact['network'], Protocol::NATIVE_SUPPORT)) { - Logger::notice(API_LOG_PREFIX . 'Not supported for {network}', ['module' => 'api', 'action' => 'friendships_destroy', 'network' => $contact['network']]); - throw new ExpectationFailedException("Not supported"); + throw new HTTPException\NotFoundException('Not following Contact'); } $dissolve = ($contact['rel'] == Contact::SHARING); - $owner = User::getOwnerDataById($uid); - if ($owner) { - Contact::terminateFriendship($owner, $contact, $dissolve); - } - else { - Logger::notice(API_LOG_PREFIX . 'No owner {uid} found', ['module' => 'api', 'action' => 'friendships_destroy', 'uid' => $uid]); - throw new NotFoundException("Error Processing Request"); - } + try { + $result = Contact::terminateFriendship($owner, $contact, $dissolve); - // Sharing-only contacts get deleted as there no relationship any more - if ($dissolve) { - Contact::remove($contact['id']); - } else { - Contact::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); + if ($result === null) { + Logger::notice(API_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.'); + } + } catch (Exception $e) { + Logger::error(API_LOG_PREFIX . $e->getMessage(), ['owner' => $owner, 'contact' => $contact, 'dissolve' => $dissolve]); + throw new HTTPException\InternalServerErrorException('Unable to unfollow this contact, please contact your administrator'); } // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($contact["uid"]); - unset($contact["self"]); + unset($contact['uid']); + unset($contact['self']); // Set screen_name since Twidere requests it - $contact["screen_name"] = $contact["nick"]; + $contact['screen_name'] = $contact['nick']; - return api_format_data("friendships-destroy", $type, ['user' => $contact]); + return api_format_data('friendships-destroy', $type, ['user' => $contact]); } api_register_func('api/friendships/destroy', 'api_friendships_destroy', true, API_METHOD_POST); diff --git a/mod/unfollow.php b/mod/unfollow.php index 2f9264088..a307c4d6e 100644 --- a/mod/unfollow.php +++ b/mod/unfollow.php @@ -120,6 +120,12 @@ function unfollow_process(string $url) $uid = local_user(); + $owner = User::getOwnerDataById($uid); + if (!$owner) { + \Friendica\Module\Security\Logout::init(); + // NOTREACHED + } + $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", $uid, Contact::SHARING, Contact::FRIEND, Strings::normaliseLink($url), Strings::normaliseLink($url), $url]; @@ -131,27 +137,30 @@ function unfollow_process(string $url) // NOTREACHED } - if (!in_array($contact['network'], Protocol::NATIVE_SUPPORT)) { - notice(DI::l10n()->t('Unfollowing is currently not supported by your network.')); - DI::baseUrl()->redirect($base_return_path . '/' . $contact['id']); - // NOTREACHED - } - $dissolve = ($contact['rel'] == Contact::SHARING); - $owner = User::getOwnerDataById($uid); - if ($owner) { - Contact::terminateFriendship($owner, $contact, $dissolve); - } - - // Sharing-only contacts get deleted as there no relationship anymore - if ($dissolve) { - Contact::remove($contact['id']); - $return_path = $base_return_path; - } else { - Contact::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); - $return_path = $base_return_path . '/' . $contact['id']; + $notice_message = ''; + $return_path = $base_return_path . '/' . $contact['id']; + + try { + $result = Contact::terminateFriendship($owner, $contact, $dissolve); + + if ($result === null) { + $notice_message = DI::l10n()->t('Unfollowing is currently not supported by this contact\'s network.'); + } + + if ($result === false) { + $notice_message = DI::l10n()->t('Unable to unfollow this contact, please retry in a few minutes or contact your administrator.'); + } + + if ($result === true) { + $notice_message = DI::l10n()->t('Contact was successfully unfollowed'); + } + } catch (Exception $e) { + DI::logger()->error($e->getMessage(), ['owner' => $owner, 'contact' => $contact, 'dissolve' => $dissolve]); + $notice_message = DI::l10n()->t('Unable to unfollow this contact, please contact your administrator'); } + notice($notice_message); DI::baseUrl()->redirect($return_path); } diff --git a/src/Console/Contact.php b/src/Console/Contact.php index 9dfcf1392..cbfd4b6c6 100644 --- a/src/Console/Contact.php +++ b/src/Console/Contact.php @@ -23,6 +23,7 @@ namespace Friendica\Console; use Console_Table; use Friendica\App; +use Friendica\DI; use Friendica\Model\Contact as ContactModel; use Friendica\Model\User as UserModel; use Friendica\Network\Probe; @@ -177,11 +178,12 @@ HELP; } /** - * Sends an unfriend message. Does not remove the contact + * Sends an unfriend message. * * @return bool True, if the command was successful + * @throws \Exception */ - private function terminateContact() + private function terminateContact(): bool { $cid = $this->getArgument(1); if (empty($cid)) { @@ -199,7 +201,23 @@ HELP; $user = UserModel::getById($contact['uid']); - $result = ContactModel::terminateFriendship($user, $contact); + try { + $result = ContactModel::terminateFriendship($user, $contact); + if ($result === null) { + throw new RuntimeException('Unfollowing is currently not supported by this contact\'s network.'); + } + + if ($result === false) { + throw new RuntimeException('Unable to unfollow this contact, please retry in a few minutes or check the logs.'); + } + + $this->out('Contact was successfully unfollowed'); + + return true; + } catch (\Exception $e) { + DI::logger()->error($e->getMessage(), ['owner' => $user, '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 b0bf72aeb..7972bf4a3 100644 --- a/src/Core/Protocol.php +++ b/src/Core/Protocol.php @@ -22,6 +22,12 @@ namespace Friendica\Core; use Friendica\DI; +use Friendica\Network\HTTPException; +use Friendica\Protocol\Activity; +use Friendica\Protocol\ActivityPub; +use Friendica\Protocol\Diaspora; +use Friendica\Protocol\OStatus; +use Friendica\Protocol\Salmon; /** * Manage compatibility with federated networks @@ -157,4 +163,63 @@ class Protocol { return $display_name . ' (' . self::getAddrFromProfileUrl($profile_url) . ')'; } + + /** + * Sends an unfriend message. Does not remove the contact + * + * @param array $user User unfriending + * @param array $contact Contact unfriended + * @param boolean $two_way Revoke eventual inbound follow as well + * @return bool|null true if successful, false if not, null if no action was performed + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function terminateFriendship(array $user, array $contact, bool $two_way = false): bool + { + 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 (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) { + // create an unfollow slap + $item = []; + $item['verb'] = Activity::O_UNFOLLOW; + $item['gravity'] = GRAVITY_ACTIVITY; + $item['follow'] = $contact['url']; + $item['body'] = ''; + $item['title'] = ''; + $item['guid'] = ''; + $item['uri-id'] = 0; + $slap = OStatus::salmon($item, $user); + + if (empty($contact['notify'])) { + throw new \InvalidArgumentException('Missing expected "notify" key in OStatus/DFRN contact'); + } + + return Salmon::slapper($user, $contact['notify'], $slap) === 0; + } elseif ($protocol == Protocol::DIASPORA) { + return Diaspora::sendUnshare($user, $contact) > 0; + } elseif ($protocol == Protocol::ACTIVITYPUB) { + if ($two_way) { + ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $user['uid']); + } + + return ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $user['uid']); + } + + // Catch-all addon hook + $hook_data = [ + 'contact' => $contact, + 'two_way' => $two_way, + 'result' => null + ]; + Hook::callAll('unfollow', $hook_data); + + return $hook_data['result']; + } } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 18e498b97..5bb0608fd 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -809,7 +809,6 @@ class Contact * Marks a contact for removal * * @param int $id contact id - * @return null * @throws HTTPException\InternalServerErrorException */ public static function remove($id) @@ -828,56 +827,26 @@ class Contact } /** - * Sends an unfriend message. Does not remove the contact + * Sends an unfriend message. Removes the contact for two-way unfriending or sharing only protocols (feed an mail) * - * @param array $user User unfriending - * @param array $contact Contact unfriended - * @param boolean $dissolve Remove the contact on the remote side - * @return void + * @param array $user User unfriending + * @param array $contact Contact unfriended + * @param boolean $two_way Revoke eventual inbound follow as well + * @return bool|null true if successful, false if not, null if no action was performed * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function terminateFriendship(array $user, array $contact, $dissolve = false) + public static function terminateFriendship(array $user, array $contact, bool $two_way = false): bool { - if (empty($contact['network'])) { - return; - } + $result = Protocol::terminateFriendship($user, $contact, $two_way); - $protocol = $contact['network']; - if (($protocol == Protocol::DFRN) && !empty($contact['protocol'])) { - $protocol = $contact['protocol']; - } - - if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) { - // create an unfollow slap - $item = []; - $item['verb'] = Activity::O_UNFOLLOW; - $item['gravity'] = GRAVITY_ACTIVITY; - $item['follow'] = $contact["url"]; - $item['body'] = ''; - $item['title'] = ''; - $item['guid'] = ''; - $item['uri-id'] = 0; - $slap = OStatus::salmon($item, $user); - - if (!empty($contact['notify'])) { - Salmon::slapper($user, $contact['notify'], $slap); - } - } elseif ($protocol == Protocol::DIASPORA) { - Diaspora::sendUnshare($user, $contact); - } elseif ($protocol == Protocol::ACTIVITYPUB) { - ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $user['uid']); - - if ($dissolve) { - ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $user['uid']); - } + if ($two_way || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { + self::remove($contact['id']); } else { - $hook_data = [ - 'contact' => $contact, - 'dissolve' => $dissolve, - ]; - Hook::callAll('unfollow', $hook_data); + self::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); } + + return $result; } /** diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 390dc932c..d69419eb1 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -703,7 +703,7 @@ class Notifier } while($contact = DBA::fetch($contacts_stmt)) { - Contact::terminateFriendship($owner, $contact, true); + Protocol::terminateFriendship($owner, $contact, true); } DBA::close($contacts_stmt); diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index 46d072b32..db44dde0c 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2021.12-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-02 11:56+0000\n" +"POT-Creation-Date: 2021-10-02 08:34-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -37,7 +37,7 @@ msgstr[1] "" msgid "Monthly posting limit of %d post reached. The post was rejected." msgstr "" -#: include/api.php:4429 mod/photos.php:89 mod/photos.php:198 mod/photos.php:626 +#: include/api.php:4430 mod/photos.php:89 mod/photos.php:198 mod/photos.php:626 #: mod/photos.php:1035 mod/photos.php:1052 mod/photos.php:1599 #: src/Model/User.php:1169 src/Model/User.php:1177 src/Model/User.php:1185 #: src/Module/Settings/Profile/Photo/Crop.php:101 @@ -718,7 +718,7 @@ msgid "OStatus support is disabled. Contact can't be added." msgstr "" #: mod/follow.php:138 src/Content/Item.php:463 src/Content/Widget.php:76 -#: src/Model/Contact.php:1077 src/Model/Contact.php:1090 +#: src/Model/Contact.php:1046 src/Model/Contact.php:1059 #: view/theme/vier/theme.php:172 msgid "Connect/Follow" msgstr "" @@ -2278,11 +2278,11 @@ msgid "" "select \"Export account\"" msgstr "" -#: mod/unfollow.php:65 mod/unfollow.php:129 +#: mod/unfollow.php:65 mod/unfollow.php:135 msgid "You aren't following this contact." msgstr "" -#: mod/unfollow.php:71 mod/unfollow.php:135 +#: mod/unfollow.php:71 msgid "Unfollowing is currently not supported by your network." msgstr "" @@ -2290,6 +2290,24 @@ msgstr "" msgid "Disconnect/Unfollow" msgstr "" +#: mod/unfollow.php:149 +msgid "Unfollowing is currently not supported by this contact's network." +msgstr "" + +#: mod/unfollow.php:153 +msgid "" +"Unable to unfollow this contact, please retry in a few minutes or contact " +"your administrator." +msgstr "" + +#: mod/unfollow.php:157 +msgid "Contact was successfully unfollowed" +msgstr "" + +#: mod/unfollow.php:161 +msgid "Unable to unfollow this contact, please contact your administrator" +msgstr "" + #: mod/videos.php:120 msgid "No videos selected" msgstr "" @@ -3002,31 +3020,31 @@ msgstr "" msgid "Follow Thread" msgstr "" -#: src/Content/Item.php:443 src/Model/Contact.php:1082 +#: src/Content/Item.php:443 src/Model/Contact.php:1051 msgid "View Status" msgstr "" -#: src/Content/Item.php:444 src/Content/Item.php:466 src/Model/Contact.php:1008 -#: src/Model/Contact.php:1074 src/Model/Contact.php:1083 +#: src/Content/Item.php:444 src/Content/Item.php:466 src/Model/Contact.php:977 +#: src/Model/Contact.php:1043 src/Model/Contact.php:1052 #: src/Module/Directory.php:160 src/Module/Settings/Profile/Index.php:223 msgid "View Profile" msgstr "" -#: src/Content/Item.php:445 src/Model/Contact.php:1084 +#: src/Content/Item.php:445 src/Model/Contact.php:1053 msgid "View Photos" msgstr "" -#: src/Content/Item.php:446 src/Model/Contact.php:1075 -#: src/Model/Contact.php:1085 +#: src/Content/Item.php:446 src/Model/Contact.php:1044 +#: src/Model/Contact.php:1054 msgid "Network Posts" msgstr "" -#: src/Content/Item.php:447 src/Model/Contact.php:1076 -#: src/Model/Contact.php:1086 +#: src/Content/Item.php:447 src/Model/Contact.php:1045 +#: src/Model/Contact.php:1055 msgid "View Contact" msgstr "" -#: src/Content/Item.php:448 src/Model/Contact.php:1088 +#: src/Content/Item.php:448 src/Model/Contact.php:1057 msgid "Send PM" msgstr "" @@ -3049,7 +3067,7 @@ msgstr "" msgid "Languages" msgstr "" -#: src/Content/Item.php:458 src/Model/Contact.php:1089 +#: src/Content/Item.php:458 src/Model/Contact.php:1058 msgid "Poke" msgstr "" @@ -3499,7 +3517,7 @@ msgstr "" msgid "Organisations" msgstr "" -#: src/Content/Widget.php:529 src/Model/Contact.php:1505 +#: src/Content/Widget.php:529 src/Model/Contact.php:1474 msgid "News" msgstr "" @@ -4363,85 +4381,85 @@ msgstr "" msgid "Legacy module file not found: %s" msgstr "" -#: src/Model/Contact.php:1078 src/Model/Contact.php:1091 +#: src/Model/Contact.php:1047 src/Model/Contact.php:1060 msgid "UnFollow" msgstr "" -#: src/Model/Contact.php:1087 +#: src/Model/Contact.php:1056 msgid "Drop Contact" msgstr "" -#: src/Model/Contact.php:1097 src/Module/Admin/Users/Pending.php:107 +#: src/Model/Contact.php:1066 src/Module/Admin/Users/Pending.php:107 #: src/Module/Notifications/Introductions.php:111 #: src/Module/Notifications/Introductions.php:183 msgid "Approve" msgstr "" -#: src/Model/Contact.php:1501 +#: src/Model/Contact.php:1470 msgid "Organisation" msgstr "" -#: src/Model/Contact.php:1509 +#: src/Model/Contact.php:1478 msgid "Forum" msgstr "" -#: src/Model/Contact.php:2365 +#: src/Model/Contact.php:2334 msgid "Disallowed profile URL." msgstr "" -#: src/Model/Contact.php:2370 src/Module/Friendica.php:81 +#: src/Model/Contact.php:2339 src/Module/Friendica.php:81 msgid "Blocked domain" msgstr "" -#: src/Model/Contact.php:2375 +#: src/Model/Contact.php:2344 msgid "Connect URL missing." msgstr "" -#: src/Model/Contact.php:2384 +#: src/Model/Contact.php:2353 msgid "" "The contact could not be added. Please check the relevant network " "credentials in your Settings -> Social Networks page." msgstr "" -#: src/Model/Contact.php:2421 +#: src/Model/Contact.php:2390 msgid "The profile address specified does not provide adequate information." msgstr "" -#: src/Model/Contact.php:2423 +#: src/Model/Contact.php:2392 msgid "No compatible communication protocols or feeds were discovered." msgstr "" -#: src/Model/Contact.php:2426 +#: src/Model/Contact.php:2395 msgid "An author or name was not found." msgstr "" -#: src/Model/Contact.php:2429 +#: src/Model/Contact.php:2398 msgid "No browser URL could be matched to this address." msgstr "" -#: src/Model/Contact.php:2432 +#: src/Model/Contact.php:2401 msgid "" "Unable to match @-style Identity Address with a known protocol or email " "contact." msgstr "" -#: src/Model/Contact.php:2433 +#: src/Model/Contact.php:2402 msgid "Use mailto: in front of address to force email check." msgstr "" -#: src/Model/Contact.php:2439 +#: src/Model/Contact.php:2408 msgid "" "The profile address specified belongs to a network which has been disabled " "on this site." msgstr "" -#: src/Model/Contact.php:2444 +#: src/Model/Contact.php:2413 msgid "" "Limited profile. This person will be unable to receive direct/personal " "notifications from you." msgstr "" -#: src/Model/Contact.php:2503 +#: src/Model/Contact.php:2472 msgid "Unable to retrieve contact information." msgstr ""