From 45c2cc3887a620c6158a1093c05bca99a5a0d1d9 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 May 2021 21:51:32 +0000 Subject: [PATCH] API: Account actions --- doc/API-Mastodon.md | 11 +++-- src/Factory/Api/Mastodon/Account.php | 6 +-- src/Factory/Api/Mastodon/Relationship.php | 22 ++++++++- src/Model/Contact.php | 48 ++++++++++++++++++- src/Module/Api/Mastodon/Accounts/Block.php | 47 ++++++++++++++++++ src/Module/Api/Mastodon/Accounts/Follow.php | 47 ++++++++++++++++++ src/Module/Api/Mastodon/Accounts/Mute.php | 47 ++++++++++++++++++ src/Module/Api/Mastodon/Accounts/Unblock.php | 47 ++++++++++++++++++ src/Module/Api/Mastodon/Accounts/Unfollow.php | 47 ++++++++++++++++++ src/Module/Api/Mastodon/Accounts/Unmute.php | 47 ++++++++++++++++++ src/Module/Api/Mastodon/Favourited.php | 2 +- src/Object/Api/Mastodon/Relationship.php | 41 +++++++++++----- static/routes.config.php | 19 ++++---- 13 files changed, 399 insertions(+), 32 deletions(-) create mode 100644 src/Module/Api/Mastodon/Accounts/Block.php create mode 100644 src/Module/Api/Mastodon/Accounts/Follow.php create mode 100644 src/Module/Api/Mastodon/Accounts/Mute.php create mode 100644 src/Module/Api/Mastodon/Accounts/Unblock.php create mode 100644 src/Module/Api/Mastodon/Accounts/Unfollow.php create mode 100644 src/Module/Api/Mastodon/Accounts/Unmute.php diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 4ec2ce005..0e1610365 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -13,17 +13,18 @@ Authentication is the same as described in [Using the APIs](help/api#Authenticat Supported mobile apps: -- [Tusky](https://tusky.app) -- [Husky](https://husky.fwgs.ru) -- [twitlatte](https://github.com/moko256/twitlatte) - [AndStatus](http://andstatus.org) -- [Twidere](https://github.com/TwidereProject/) +- [Husky](https://husky.fwgs.ru) - [Subway Tooter](https://github.com/tateisu/SubwayTooter) +- [Tusky](https://tusky.app) +- [Twidere](https://github.com/TwidereProject/) +- [twitlatte](https://github.com/moko256/twitlatte) +- [Yuito](https://github.com/accelforce/Yuito) Unsupported mobile apps: -- [Mammut](https://github.com/jamiesanson/Mammut) There are problems with the token request, see issue https://github.com/jamiesanson/Mammut/issues/19 - [Fedilab](https://framagit.org/tom79/fedilab) Automatically uses the legacy API, see issue: https://framagit.org/tom79/fedilab/-/issues/520 +- [Mammut](https://github.com/jamiesanson/Mammut) There are problems with the token request, see issue https://github.com/jamiesanson/Mammut/issues/19 ## Entities diff --git a/src/Factory/Api/Mastodon/Account.php b/src/Factory/Api/Mastodon/Account.php index 40f55c6c3..28335a0e8 100644 --- a/src/Factory/Api/Mastodon/Account.php +++ b/src/Factory/Api/Mastodon/Account.php @@ -44,9 +44,9 @@ class Account extends BaseFactory { parent::__construct($logger); - $this->baseUrl = $baseURL; + $this->baseUrl = $baseURL; $this->profileField = $profileField; - $this->mstdnField = $mstdnField; + $this->mstdnField = $mstdnField; } /** @@ -61,7 +61,7 @@ class Account extends BaseFactory $cdata = Contact::getPublicAndUserContacID($contactId, $uid); if (!empty($cdata)) { $publicContact = Contact::getById($cdata['public']); - $userContact = Contact::getById($cdata['user']); + $userContact = Contact::getById($cdata['user']); } else { $publicContact = Contact::getById($contactId); $userContact = []; diff --git a/src/Factory/Api/Mastodon/Relationship.php b/src/Factory/Api/Mastodon/Relationship.php index 1b1cc0f33..a1d94a332 100644 --- a/src/Factory/Api/Mastodon/Relationship.php +++ b/src/Factory/Api/Mastodon/Relationship.php @@ -37,13 +37,33 @@ class Relationship extends BaseFactory return $this->createFromContact(Contact::getById($userContactId)); } + /** + * @param int $publicContactId Contact row id with uid = 0 + * @param int $uid User ID + * @return RelationshipEntity + * @throws \Exception + */ + public function createFromPublicContactId(int $publicContactId, int $uid) + { + $cdata = Contact::getPublicAndUserContacID($publicContactId, $uid); + if (!empty($cdata)) { + $cid = $cdata['user']; + } else { + $cid = $publicContactId; + } + + return $this->createFromContact(Contact::getById($cid)); + } + /** * @param array $userContact Full contact row record with uid != 0 * @return RelationshipEntity */ public function createFromContact(array $userContact) { - return new RelationshipEntity($userContact['id'], $userContact); + return new RelationshipEntity($userContact['id'], $userContact, + Contact\User::isBlocked($userContact['id'], $userContact['uid']), + Contact\User::isIgnored($userContact['id'], $userContact['uid'])); } /** diff --git a/src/Model/Contact.php b/src/Model/Contact.php index f75cffada..89325e093 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -2409,6 +2409,50 @@ class Contact return $contact; } + /** + * Follow a contact + * + * @param int $cid Public contact id + * @param int $uid User ID + * + * @return bool "true" if following had been successful + */ + public static function follow(int $cid, int $uid) + { + $user = User::getById($uid); + if (empty($user)) { + return false; + } + + $contact = self::getById($cid, ['url']); + + $result = self::createFromProbe($user, $contact['url'], false); + + return $result['cid']; + } + + /** + * 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::getPublicAndUserContacID($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. @@ -2554,7 +2598,7 @@ class Contact return null; } - public static function removeFollower($importer, $contact, array $datarray = [], $item = "") + public static function removeFollower($importer, $contact) { if (($contact['rel'] == self::FRIEND) || ($contact['rel'] == self::SHARING)) { DBA::update('contact', ['rel' => self::SHARING], ['id' => $contact['id']]); @@ -2563,7 +2607,7 @@ class Contact } } - public static function removeSharer($importer, $contact, array $datarray = [], $item = "") + public static function removeSharer($importer, $contact) { if (($contact['rel'] == self::FRIEND) || ($contact['rel'] == self::FOLLOWER)) { DBA::update('contact', ['rel' => self::FOLLOWER], ['id' => $contact['id']]); diff --git a/src/Module/Api/Mastodon/Accounts/Block.php b/src/Module/Api/Mastodon/Accounts/Block.php new file mode 100644 index 000000000..504345244 --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Block.php @@ -0,0 +1,47 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Block extends BaseApi +{ + public static function post(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + Contact\User::setBlocked($parameters['id'], $uid, true); + + System::jsonExit(DI::mstdnRelationship()->createFromPublicContactId($parameters['id'], $uid)->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Follow.php b/src/Module/Api/Mastodon/Accounts/Follow.php new file mode 100644 index 000000000..921fac69a --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Follow.php @@ -0,0 +1,47 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Follow extends BaseApi +{ + public static function post(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + $cid = Contact::follow($parameters['id'], self::getCurrentUserID()); + + System::jsonExit(DI::mstdnRelationship()->createFromContactId($cid)->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Mute.php b/src/Module/Api/Mastodon/Accounts/Mute.php new file mode 100644 index 000000000..6dda625f0 --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Mute.php @@ -0,0 +1,47 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Mute extends BaseApi +{ + public static function post(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + Contact\User::setIgnored($parameters['id'], $uid, true); + + System::jsonExit(DI::mstdnRelationship()->createFromPublicContactId($parameters['id'], $uid)->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Unblock.php b/src/Module/Api/Mastodon/Accounts/Unblock.php new file mode 100644 index 000000000..51997453f --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Unblock.php @@ -0,0 +1,47 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Unblock extends BaseApi +{ + public static function post(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + Contact\User::setBlocked($parameters['id'], $uid, false); + + System::jsonExit(DI::mstdnRelationship()->createFromPublicContactId($parameters['id'], $uid)->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Unfollow.php b/src/Module/Api/Mastodon/Accounts/Unfollow.php new file mode 100644 index 000000000..ef681e67f --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Unfollow.php @@ -0,0 +1,47 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Unfollow extends BaseApi +{ + public static function post(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + Contact::unfollow($parameters['id'], $uid); + + System::jsonExit(DI::mstdnRelationship()->createFromPublicContactId($parameters['id'], $uid)->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Unmute.php b/src/Module/Api/Mastodon/Accounts/Unmute.php new file mode 100644 index 000000000..c870da0aa --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Unmute.php @@ -0,0 +1,47 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Unmute extends BaseApi +{ + public static function post(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + Contact\User::setIgnored($parameters['id'], $uid, false); + + System::jsonExit(DI::mstdnRelationship()->createFromPublicContactId($parameters['id'], $uid)->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Favourited.php b/src/Module/Api/Mastodon/Favourited.php index f46fb7b07..53bd82aab 100644 --- a/src/Module/Api/Mastodon/Favourited.php +++ b/src/Module/Api/Mastodon/Favourited.php @@ -52,7 +52,7 @@ class Favourited extends BaseApi $params = ['order' => ['thr-parent-id' => true], 'limit' => $limit]; - $condition = ['gravity' => GRAVITY_ACTIVITY, 'verb' => Activity::LIKE, 'uid' => $uid]; + $condition = ['gravity' => GRAVITY_ACTIVITY, 'origin' => true, 'verb' => Activity::LIKE, 'uid' => $uid]; if (!empty($max_id)) { $condition = DBA::mergeConditions($condition, ["`thr-parent-id` < ?", $max_id]); diff --git a/src/Object/Api/Mastodon/Relationship.php b/src/Object/Api/Mastodon/Relationship.php index 16a4a2d30..463eb97e0 100644 --- a/src/Object/Api/Mastodon/Relationship.php +++ b/src/Object/Api/Mastodon/Relationship.php @@ -37,42 +37,61 @@ class Relationship extends BaseDataTransferObject /** @var bool */ protected $following = false; /** @var bool */ - protected $followed_by = false; + protected $requested = false; + /** + * Unsupported + * @var bool + */ + protected $endorsed = false; /** @var bool */ - protected $blocking = false; + protected $followed_by = false; /** @var bool */ protected $muting = false; /** @var bool */ protected $muting_notifications = false; + /** + * Unsupported + * @var bool + */ + protected $showing_reblogs = true; /** @var bool */ - protected $requested = false; + protected $notifying = false; + /** @var bool */ + protected $blocking = false; /** @var bool */ protected $domain_blocking = false; /** * Unsupported * @var bool */ - protected $showing_reblogs = true; + protected $blocked_by = false; /** * Unsupported - * @var bool + * @var string */ - protected $endorsed = false; + protected $note = ''; /** * @param int $userContactId Contact row Id with uid != 0 * @param array $userContact Full Contact table record with uid != 0 + * @param bool $blocked "true" if user is blocked + * @param bool $muted "true" if user is muted */ - public function __construct(int $userContactId, array $userContact = []) + public function __construct(int $userContactId, array $userContact = [], bool $blocked = false, bool $muted = false) { $this->id = $userContactId; $this->following = in_array($userContact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]); - $this->followed_by = in_array($userContact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]); - $this->blocking = (bool)$userContact['blocked'] ?? false; - $this->muting = (bool)$userContact['readonly'] ?? false; - $this->muting_notifications = (bool)$userContact['readonly'] ?? false; $this->requested = (bool)$userContact['pending'] ?? false; + $this->endorsed = false; + $this->followed_by = in_array($userContact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]); + $this->muting = (bool)($userContact['readonly'] ?? false) || $muted; + $this->muting_notifications = $this->muting; + $this->showing_reblogs = true; + $this->notifying = (bool)$userContact['notify_new_posts'] ?? false; + $this->blocking = (bool)($userContact['blocked'] ?? false) || $blocked; $this->domain_blocking = Network::isUrlBlocked($userContact['url'] ?? ''); + $this->blocked_by = false; + $this->note = ''; return $this; } diff --git a/static/routes.config.php b/static/routes.config.php index 3a19ff379..7c33d5674 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -64,12 +64,12 @@ return [ '/accounts/{id:\d+}/following' => [Module\Api\Mastodon\Accounts\Following::class, [R::GET ]], '/accounts/{id:\d+}/lists' => [Module\Api\Mastodon\Accounts\Lists::class, [R::GET ]], '/accounts/{id:\d+}/identity_proofs' => [Module\Api\Mastodon\Accounts\IdentityProofs::class, [R::GET ]], // Dummy, not supported - '/accounts/{id:\d+}/follow' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo - '/accounts/{id:\d+}/unfollow' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo - '/accounts/{id:\d+}/block' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo - '/accounts/{id:\d+}/unblock' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo - '/accounts/{id:\d+}/mute' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo - '/accounts/{id:\d+}/unmute' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo + '/accounts/{id:\d+}/follow' => [Module\Api\Mastodon\Accounts\Follow::class, [ R::POST]], + '/accounts/{id:\d+}/unfollow' => [Module\Api\Mastodon\Accounts\Unfollow::class, [ R::POST]], + '/accounts/{id:\d+}/block' => [Module\Api\Mastodon\Accounts\Block::class, [ R::POST]], + '/accounts/{id:\d+}/unblock' => [Module\Api\Mastodon\Accounts\Unblock::class, [ R::POST]], + '/accounts/{id:\d+}/mute' => [Module\Api\Mastodon\Accounts\Mute::class, [ R::POST]], + '/accounts/{id:\d+}/unmute' => [Module\Api\Mastodon\Accounts\Unmute::class, [ R::POST]], '/accounts/{id:\d+}/pin' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported '/accounts/{id:\d+}/unpin' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported '/accounts/{id:\d+}/note' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo @@ -90,9 +90,9 @@ return [ '/apps/verify_credentials' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo '/blocks' => [Module\Api\Mastodon\Blocks::class, [R::GET ]], '/bookmarks' => [Module\Api\Mastodon\Bookmarks::class, [R::GET ]], - '/conversations' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented - '/conversations/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::DELETE ]], // not implemented - '/conversations/{id:\d+}/read' => [Module\Api\Mastodon\Unimplemented::class, [R::POST ]], // not implemented + '/conversations' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo + '/conversations/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::DELETE ]], // @todo + '/conversations/{id:\d+}/read' => [Module\Api\Mastodon\Unimplemented::class, [R::POST ]], // @todo '/custom_emojis' => [Module\Api\Mastodon\CustomEmojis::class, [R::GET ]], '/domain_blocks' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST, R::DELETE]], // not supported '/directory' => [Module\Api\Mastodon\Directory::class, [R::GET ]], @@ -142,6 +142,7 @@ return [ '/statuses/{id:\d+}/unpin' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // @todo '/suggestions' => [Module\Api\Mastodon\Suggestions::class, [R::GET ]], '/suggestions/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::DELETE ]], // not implemented + '/timelines/direct' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo '/timelines/home' => [Module\Api\Mastodon\Timelines\Home::class, [R::GET ]], '/timelines/list/{id:\d+}' => [Module\Api\Mastodon\Timelines\ListTimeline::class, [R::GET ]], '/timelines/public' => [Module\Api\Mastodon\Timelines\PublicTimeline::class, [R::GET ]],