From ff28044cf6d3b749d06bd9f66c4dc455dd68cb53 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 23 Jan 2023 19:00:20 +0000 Subject: [PATCH 1/4] Issue 12603: Support quotes in the API --- src/Factory/Api/Mastodon/Status.php | 8 +++++++- src/Object/Api/Mastodon/Status.php | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index b32a6c510f..bac9d18035 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -234,7 +234,13 @@ class Status extends BaseFactory $reshare = []; } - return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $reshare, $poll); + if (!empty($item['quote-uri-id'])) { + $quote = $this->createFromUriId($item['quote-uri-id'], $uid, false)->toArray(); + } else { + $quote = []; + } + + return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $reshare, $quote, $poll); } /** diff --git a/src/Object/Api/Mastodon/Status.php b/src/Object/Api/Mastodon/Status.php index 1e778a7108..2fcbb10e3a 100644 --- a/src/Object/Api/Mastodon/Status.php +++ b/src/Object/Api/Mastodon/Status.php @@ -75,6 +75,8 @@ class Status extends BaseDataTransferObject protected $content; /** @var Status|null */ protected $reblog = null; + /** @var Status|null */ + protected $quote = null; /** @var Application */ protected $application = null; /** @var Account */ @@ -98,7 +100,7 @@ class Status extends BaseDataTransferObject * @param array $item * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $reblog, array $poll = null) + public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $reblog, array $quote = null, array $poll = null) { $this->id = (string)$item['uri-id']; $this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::JSON); @@ -134,6 +136,7 @@ class Status extends BaseDataTransferObject $this->pinned = $userAttributes->pinned; $this->content = BBCode::convertForUriId($item['uri-id'], BBCode::setMentionsToNicknames($item['raw-body'] ?? $item['body']), BBCode::MASTODON_API); $this->reblog = $reblog; + $this->quote = $quote; $this->application = $application->toArray(); $this->account = $account->toArray(); $this->media_attachments = $attachments; @@ -165,6 +168,10 @@ class Status extends BaseDataTransferObject $status['reblog'] = null; } + if (empty($status['quote'])) { + $status['quote'] = null; + } + return $status; } } From d0373ab41436c89c52558245758b510b417ffd71 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 23 Jan 2023 21:24:50 +0000 Subject: [PATCH 2/4] Issue 12603: Support quote in the api / Issue 12654: prevent privacy leakage --- src/Factory/Api/Mastodon/Status.php | 71 +++++++++++++++++++++++----- src/Module/Api/Mastodon/Statuses.php | 15 +++++- src/Object/Api/Mastodon/Status.php | 11 ++++- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index bac9d18035..a72406788b 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -24,6 +24,7 @@ namespace Friendica\Factory\Api\Mastodon; use Friendica\BaseFactory; use Friendica\Content\ContactSelector; use Friendica\Content\Item as ContentItem; +use Friendica\Content\Text\BBCode; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Model\Item; @@ -76,15 +77,16 @@ class Status extends BaseFactory } /** - * @param int $uriId Uri-ID of the item - * @param int $uid Item user - * @param bool $reblog Check for reblogged post + * @param int $uriId Uri-ID of the item + * @param int $uid Item user + * @param bool $reblog Check for reblogged post + * @param bool $in_reply_status Add an "in_reply_status" element * * @return \Friendica\Object\Api\Mastodon\Status * @throws HTTPException\InternalServerErrorException * @throws ImagickException|HTTPException\NotFoundException */ - public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true): \Friendica\Object\Api\Mastodon\Status + public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true, bool $in_reply_status = true, bool $diplay_quote = false): \Friendica\Object\Api\Mastodon\Status { $fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', 'created', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id']; @@ -222,25 +224,67 @@ class Status extends BaseFactory } } - $item['body'] = $this->contentItem->addSharedPost($item); + if ($diplay_quote) { + $quote = self::createQuote($item, $uid); - if (!is_null($item['raw-body'])) { - $item['raw-body'] = $this->contentItem->addSharedPost($item, $item['raw-body']); + $item['body'] = BBCode::removeSharedData($item['body']); + + if (!is_null($item['raw-body'])) { + $item['raw-body'] = BBCode::removeSharedData($item['raw-body']); + } + } else { + // We can always safely add attached activities. Real quotes are added to the body via "addSharedPost". + if (empty($item['quote-uri-id'])) { + $quote = self::createQuote($item, $uid); + } + + $item['body'] = $this->contentItem->addSharedPost($item); + + if (!is_null($item['raw-body'])) { + $item['raw-body'] = $this->contentItem->addSharedPost($item, $item['raw-body']); + } } if ($is_reshare) { - $reshare = $this->createFromUriId($uriId, $uid, false)->toArray(); + $reshare = $this->createFromUriId($uriId, $uid, false, false)->toArray(); } else { $reshare = []; } - if (!empty($item['quote-uri-id'])) { - $quote = $this->createFromUriId($item['quote-uri-id'], $uid, false)->toArray(); + if ($in_reply_status && ($item['gravity'] == Item::GRAVITY_COMMENT)) { + $in_reply = $this->createFromUriId($item['thr-parent-id'], $uid, false, false)->toArray(); + } else { + $in_reply = []; + } + + return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $quote, $poll); + } + + /** + * Create a quote status object + * + * @param array $item + * @param integer $uid + * @return array + */ + private function createQuote(array $item, int $uid): array + { + if (empty($item['quote-uri-id'])) { + $media = Post\Media::getByURIId($item['uri-id'], [Post\Media::ACTIVITY]); + if (!empty($media)) { + $shared_item = Post::selectFirst(['uri-id'], ['plink' => $media[0]['url'], 'uid' => [$uid, 0]]); + $quote_id = $shared_item['uri-id']; + } + } else { + $quote_id = $item['quote-uri-id']; + } + + if (!empty($quote_id)) { + $quote = $this->createFromUriId($quote_id, $uid, false, false)->toArray(); } else { $quote = []; } - - return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $reshare, $quote, $poll); + return $quote; } /** @@ -271,8 +315,9 @@ class Status extends BaseFactory $tags = []; $card = new \Friendica\Object\Api\Mastodon\Card([]); $attachments = []; + $in_reply = []; $reshare = []; - return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $reshare); + return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare); } } diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 71681968db..7419cccb38 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -101,6 +101,7 @@ class Statuses extends BaseApi 'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used. 'poll' => [], // Poll data. If provided, media_ids cannot be used, and poll[expires_in] must be provided. 'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply + 'quote_id' => 0, // ID of the message to quote 'sensitive' => false, // Mark status and attached media as sensitive? 'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. 'visibility' => '', // Visibility of the posted status. One of: "public", "unlisted", "private" or "direct". @@ -153,6 +154,7 @@ class Statuses extends BaseApi $item['private'] = Item::PRIVATE; break; case 'direct': + $item['private'] = Item::PRIVATE; // The permissions are assigned in "expandTags" break; default: @@ -183,11 +185,15 @@ class Statuses extends BaseApi } if ($request['in_reply_to_id']) { - $parent = Post::selectFirst(['uri'], ['uri-id' => $request['in_reply_to_id'], 'uid' => [0, $uid]]); + $parent = Post::selectFirst(['uri', 'private'], ['uri-id' => $request['in_reply_to_id'], 'uid' => [0, $uid]]); $item['thr-parent'] = $parent['uri']; $item['gravity'] = Item::GRAVITY_COMMENT; $item['object-type'] = Activity\ObjectType::COMMENT; + + if (in_array($parent['private'], [Item::UNLISTED, Item::PUBLIC]) && ($item['private'] == Item::PRIVATE)) { + throw new HTTPException\NotImplementedException('Private replies for public posts are not implemented.'); + } } else { self::checkThrottleLimit(); @@ -195,6 +201,13 @@ class Statuses extends BaseApi $item['object-type'] = Activity\ObjectType::NOTE; } + if ($request['quote_id']) { + if (!Post::exists(['uri-id' => $request['quote_id'], 'uid' => [0, $uid]])) { + throw new HTTPException\NotFoundException('Item with URI ID ' . $request['quote_id'] . ' not found for user ' . $uid . '.'); + } + $item['quote-uri-id'] = $request['quote_id']; + } + if (!empty($request['spoiler_text'])) { if (!$request['in_reply_to_id'] && DI::pConfig()->get($uid, 'system', 'api_spoiler_title', true)) { $item['title'] = $request['spoiler_text']; diff --git a/src/Object/Api/Mastodon/Status.php b/src/Object/Api/Mastodon/Status.php index 2fcbb10e3a..e3f322778d 100644 --- a/src/Object/Api/Mastodon/Status.php +++ b/src/Object/Api/Mastodon/Status.php @@ -41,6 +41,8 @@ class Status extends BaseDataTransferObject protected $created_at; /** @var string|null */ protected $in_reply_to_id = null; + /** @var Status|null - Fedilab extension, see issue https://github.com/friendica/friendica/issues/12672 */ + protected $in_reply_to_status = null; /** @var string|null */ protected $in_reply_to_account_id = null; /** @var bool */ @@ -75,7 +77,7 @@ class Status extends BaseDataTransferObject protected $content; /** @var Status|null */ protected $reblog = null; - /** @var Status|null */ + /** @var Status|null - Akkoma extension, see issue https://github.com/friendica/friendica/issues/12603 */ protected $quote = null; /** @var Application */ protected $application = null; @@ -100,13 +102,14 @@ class Status extends BaseDataTransferObject * @param array $item * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $reblog, array $quote = null, array $poll = null) + public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $in_reply, array $reblog, array $quote = null, array $poll = null) { $this->id = (string)$item['uri-id']; $this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::JSON); if ($item['gravity'] == Item::GRAVITY_COMMENT) { $this->in_reply_to_id = (string)$item['thr-parent-id']; + $this->in_reply_to_status = $in_reply; $this->in_reply_to_account_id = (string)$item['parent-author-id']; } @@ -172,6 +175,10 @@ class Status extends BaseDataTransferObject $status['quote'] = null; } + if (empty($status['in_reply_to_status'])) { + $status['in_reply_to_status'] = null; + } + return $status; } } From c471a78b055f5e83e269e442a6970a4a5a9f7ae1 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 23 Jan 2023 21:44:30 +0000 Subject: [PATCH 3/4] Issue 12358: Allow blocking of every contact --- src/Module/Api/Mastodon/Accounts/Block.php | 23 +++++++--------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Module/Api/Mastodon/Accounts/Block.php b/src/Module/Api/Mastodon/Accounts/Block.php index 43d56a3b68..e13a8b2ff4 100644 --- a/src/Module/Api/Mastodon/Accounts/Block.php +++ b/src/Module/Api/Mastodon/Accounts/Block.php @@ -41,26 +41,17 @@ class Block extends BaseApi DI::mstdnError()->UnprocessableEntity(); } - $owner = User::getOwnerDataById($uid); - if (empty($owner)) { - DI::mstdnError()->Forbidden(); - } + Contact\User::setBlocked($this->parameters['id'], $uid, true); $cdata = Contact::getPublicAndUserContactID($this->parameters['id'], $uid); - if (empty($cdata['user'])) { - DI::mstdnError()->RecordNotFound(); + if (!empty($cdata['user'])) { + $contact = Contact::getById($cdata['user']); + if (!empty($contact)) { + // Mastodon-expected behavior: relationship is severed on block + Contact::terminateFriendship($contact); + } } - $contact = Contact::getById($cdata['user']); - if (empty($contact)) { - DI::mstdnError()->RecordNotFound(); - } - - Contact\User::setBlocked($cdata['user'], $uid, true); - - // Mastodon-expected behavior: relationship is severed on block - Contact::terminateFriendship($contact); - System::jsonExit(DI::mstdnRelationship()->createFromContactId($this->parameters['id'], $uid)->toArray()); } } From b61923700e2f798ca2d1c2064af06b8814661bf6 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 23 Jan 2023 22:30:28 +0000 Subject: [PATCH 4/4] Display, not diplay --- src/Factory/Api/Mastodon/Status.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index a72406788b..1b569d57e5 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -81,12 +81,13 @@ class Status extends BaseFactory * @param int $uid Item user * @param bool $reblog Check for reblogged post * @param bool $in_reply_status Add an "in_reply_status" element + * @param bool $display_quote Display quoted posts * * @return \Friendica\Object\Api\Mastodon\Status * @throws HTTPException\InternalServerErrorException * @throws ImagickException|HTTPException\NotFoundException */ - public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true, bool $in_reply_status = true, bool $diplay_quote = false): \Friendica\Object\Api\Mastodon\Status + public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true, bool $in_reply_status = true, bool $display_quote = false): \Friendica\Object\Api\Mastodon\Status { $fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', 'created', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id']; @@ -224,7 +225,7 @@ class Status extends BaseFactory } } - if ($diplay_quote) { + if ($display_quote) { $quote = self::createQuote($item, $uid); $item['body'] = BBCode::removeSharedData($item['body']);