From 9bf7529dda7b936b86cabd74f0976de433058f50 Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Sun, 12 Nov 2023 20:59:49 +0800 Subject: [PATCH 1/6] Improve emoji federation and mastodon api compliance --- src/Content/Smilies.php | 28 ++++++++++++++++++++++++ src/Factory/Api/Mastodon/Emoji.php | 21 ++++++++++++++---- src/Factory/Api/Mastodon/Status.php | 10 ++++++++- src/Object/Api/Mastodon/Status.php | 4 ++-- src/Protocol/ActivityPub/Transmitter.php | 23 +++++++++++++++++++ 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/Content/Smilies.php b/src/Content/Smilies.php index 2e1a6cf19c..760bfbce9e 100644 --- a/src/Content/Smilies.php +++ b/src/Content/Smilies.php @@ -152,6 +152,34 @@ class Smilies return $params; } + /** + * Finds all used smilies (like :heart: or :p) in the provided text. + * + * @param string $text that might contain smilie usages (denoted by a starting colon) + * @param bool $extract_url whether to further extract image urls + * @return array with smilie codes (colon included) as the keys, the smilie images as values + */ + public static function extractUsedSmilies(string $text, bool $extract_url = false): array + { + $emojis = []; + + $smilies = self::getList(); + $icons = $smilies['icons']; + foreach ($smilies['texts'] as $i => $name) { + if (strstr($text, $name)) { + $image = $icons[$i]; + if ($extract_url) { + if (preg_match('/src="(.+?)"/', $image, $match)) { + $image = $match[1]; + } else { + continue; + } + } + $emojis[$name] = $image; + } + } + return $emojis; + } /** * Copied from http://php.net/manual/en/function.str-replace.php#88569 diff --git a/src/Factory/Api/Mastodon/Emoji.php b/src/Factory/Api/Mastodon/Emoji.php index 157b91bf91..b7f4ee6ead 100644 --- a/src/Factory/Api/Mastodon/Emoji.php +++ b/src/Factory/Api/Mastodon/Emoji.php @@ -32,19 +32,22 @@ class Emoji extends BaseFactory } /** + * Creates an emoji collection from shortcode => image mappings. + * * @param array $smilies * * @return Emojis */ - public function createCollectionFromSmilies(array $smilies): Emojis + public function createCollectionFromArray(array $smilies): Emojis { $prototype = null; $emojis = []; - foreach ($smilies['texts'] as $key => $shortcode) { - if (preg_match('/src="(.+?)"/', $smilies['icons'][$key], $matches)) { + foreach ($smilies as $shortcode => $icon) { + if (preg_match('/src="(.+?)"/', $icon, $matches)) { $url = $matches[1]; + $shortcode = trim($shortcode, ':'); if ($prototype === null) { $prototype = $this->create($shortcode, $url); @@ -52,9 +55,19 @@ class Emoji extends BaseFactory } else { $emojis[] = \Friendica\Object\Api\Mastodon\Emoji::createFromPrototype($prototype, $shortcode, $url); } - }; + } } return new Emojis($emojis); } + + /** + * @param array $smilies + * + * @return Emojis + */ + public function createCollectionFromSmilies(array $smilies): Emojis + { + return self::createCollectionFromArray(array_combine($smilies['texts'], $smilies['icons'])); + } } diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index 4bf5609b9a..aaaa8d3d94 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\Smilies; use Friendica\Content\Text\BBCode; use Friendica\Core\Logger; use Friendica\Database\Database; @@ -57,6 +58,8 @@ class Status extends BaseFactory private $mstdnCardFactory; /** @var Attachment */ private $mstdnAttachmentFactory; + /** @var Emoji */ + private $mstdnEmojiFactory; /** @var Error */ private $mstdnErrorFactory; /** @var Poll */ @@ -74,6 +77,7 @@ class Status extends BaseFactory Tag $mstdnTagFactory, Card $mstdnCardFactory, Attachment $mstdnAttachmentFactory, + Emoji $mstdnEmojiFactory, Error $mstdnErrorFactory, Poll $mstdnPollFactory, ContentItem $contentItem, @@ -86,6 +90,7 @@ class Status extends BaseFactory $this->mstdnTagFactory = $mstdnTagFactory; $this->mstdnCardFactory = $mstdnCardFactory; $this->mstdnAttachmentFactory = $mstdnAttachmentFactory; + $this->mstdnEmojiFactory = $mstdnEmojiFactory; $this->mstdnErrorFactory = $mstdnErrorFactory; $this->mstdnPollFactory = $mstdnPollFactory; $this->contentItem = $contentItem; @@ -283,6 +288,9 @@ class Status extends BaseFactory } } + $used_smilies = Smilies::extractUsedSmilies($item['body'] ?: $item['raw-body']); + $emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true); + if ($is_reshare) { try { $reshare = $this->createFromUriId($uriId, $uid, $display_quote, false, false)->toArray(); @@ -309,7 +317,7 @@ class Status extends BaseFactory $visibility_data = $uid != $item['uid'] ? null : new FriendicaVisibility($this->aclFormatter->expand($item['allow_cid']), $this->aclFormatter->expand($item['deny_cid']), $this->aclFormatter->expand($item['allow_gid']), $this->aclFormatter->expand($item['deny_gid'])); $friendica = new FriendicaExtension($item['title'] ?? '', $item['changed'], $item['commented'], $item['received'], $counts->dislikes, $origin_dislike, $delivery_data, $visibility_data); - return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll); + return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments, $in_reply, $reshare, $friendica, $quote, $poll, $emojis); } /** diff --git a/src/Object/Api/Mastodon/Status.php b/src/Object/Api/Mastodon/Status.php index 122e62f9ca..59d2a6cc58 100644 --- a/src/Object/Api/Mastodon/Status.php +++ b/src/Object/Api/Mastodon/Status.php @@ -107,7 +107,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 $in_reply, array $reblog, FriendicaExtension $friendica, 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, FriendicaExtension $friendica, array $quote = null, array $poll = null, array $emojis = null) { $reblogged = !empty($reblog); $this->id = (string)$item['uri-id']; @@ -152,7 +152,7 @@ class Status extends BaseDataTransferObject $this->media_attachments = $reblogged ? [] : $attachments; $this->mentions = $reblogged ? [] : $mentions; $this->tags = $reblogged ? [] : $tags; - $this->emojis = $reblogged ? [] : []; + $this->emojis = $reblogged ? [] : ($emojis ?: []); $this->card = $reblogged ? null : ($card->toArray() ?: null); $this->poll = $reblogged ? null : $poll; $this->friendica = $reblogged ? null : $friendica; diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index b18e247eac..130aa3ab08 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -23,6 +23,7 @@ namespace Friendica\Protocol\ActivityPub; use Friendica\App; use Friendica\Content\Feature; +use Friendica\Content\Smilies; use Friendica\Content\Text\BBCode; use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\Logger; @@ -1506,6 +1507,26 @@ class Transmitter return $location; } + /** + * Appends emoji tags to a tag array according to the tags used. + * + * @param array $tags Tag array + * @param string $text Text containing tags like :tag: + */ + private static function addEmojiTags(array &$tags, string $text) + { + foreach (Smilies::extractUsedSmilies($text, true) as $name => $url) { + $tags[] = [ + 'type' => 'Emoji', + 'name' => $name, + 'icon' => [ + 'type' => 'Image', + 'url' => $url, + ], + ]; + } + } + /** * Returns a tag array for a given item array * @@ -1538,6 +1559,8 @@ class Transmitter } } + self::addEmojiTags($tags, $item['body']); + $announce = self::getAnnounceArray($item); // Mention the original author upon commented reshares if (!empty($announce['comment'])) { From 917b801eb68eb3d382bb3752e117b57dbe07d034 Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Sun, 12 Nov 2023 22:17:37 +0800 Subject: [PATCH 2/6] Extract emojis into mastodon api only for local posts --- src/Factory/Api/Mastodon/Status.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index aaaa8d3d94..4cdc8a78fc 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -288,8 +288,11 @@ class Status extends BaseFactory } } - $used_smilies = Smilies::extractUsedSmilies($item['body'] ?: $item['raw-body']); - $emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true); + $emojis = null; + if (DI::baseUrl()->isLocalUrl($item['uri'])) { + $used_smilies = Smilies::extractUsedSmilies($item['body'] ?: $item['raw-body']); + $emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true); + } if ($is_reshare) { try { From e088bb722bb45d8dd3b0357da80c1130b89acb08 Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Sun, 12 Nov 2023 23:02:21 +0800 Subject: [PATCH 3/6] Fix editor layout when smilies overflow or on mobile --- view/theme/frio/css/style.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/view/theme/frio/css/style.css b/view/theme/frio/css/style.css index adfbf140b5..3d4d9f73dd 100644 --- a/view/theme/frio/css/style.css +++ b/view/theme/frio/css/style.css @@ -1413,6 +1413,9 @@ section #jotOpen { max-height: calc(100vh - 62px); } } +#jot-modal #jot-modal-body { + overflow: auto; +} #jot-modal #jot-sections, #jot-modal #jot-modal-body, #jot-modal #profile-jot-form, @@ -1423,7 +1426,6 @@ section #jotOpen { #jot-modal #item-Q0, #jot-modal #profile-jot-acl-wrapper, #jot-modal #acl-wrapper { - overflow: hidden; display: flex; flex: auto; flex-direction: column; From 2cb0027f5690fe11aee973782283d0c4b2c0749a Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Tue, 14 Nov 2023 10:52:34 +0800 Subject: [PATCH 4/6] Pass emojis in remote mastodon posts in mastodon api --- src/Content/Text/BBCode.php | 14 +++++++++----- src/Factory/Api/Mastodon/Emoji.php | 17 +++++++++++++---- src/Factory/Api/Mastodon/Status.php | 4 ++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 88df9511d4..fab4fba023 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -1234,7 +1234,7 @@ class BBCode } /** - * Expand Youtube and Vimeo links to + * Expand Youtube and Vimeo links to * * @param string $text * @return string @@ -1387,7 +1387,7 @@ class BBCode "\n[hr]", "[hr]\n", " [hr]", "[hr] ", "\n[attachment ", " [attachment ", "\n[/attachment]", "[/attachment]\n", " [/attachment]", "[/attachment] ", "[table]\n", "[table] ", " [table]", "\n[/table]", " [/table]", "[/table] ", - " \n", "\t\n", "[/li]\n", "\n[li]", "\n[*]", + " \n", "\t\n", "[/li]\n", "\n[li]", "\n[*]", ]; $replace = [ "[th]", "[th]", "[th]", "[/th]", "[/th]", "[/th]", @@ -1480,14 +1480,14 @@ class BBCode if ($simple_html == self::INTERNAL) { //Ensure to always start with

if possible $heading_count = 0; - for ($level = 6; $level > 0; $level--) { + for ($level = 6; $level > 0; $level--) { if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) { $heading_count++; } } if ($heading_count > 0) { $heading = min($heading_count + 3, 6); - for ($level = 6; $level > 0; $level--) { + for ($level = 6; $level > 0; $level--) { if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) { $text = preg_replace("(\[h$level\](.*?)\[\/h$level\])ism", "

$1

", $text); $heading--; @@ -1548,7 +1548,11 @@ class BBCode $text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '$2', $text); // Mastodon Emoji (internal tag, do not document for users) - $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text); + if ($simple_html == self::MASTODON_API) { + $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text); + } else { + $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text); + } // Check for CSS classes // @deprecated since 2021.12, left for backward-compatibility reasons diff --git a/src/Factory/Api/Mastodon/Emoji.php b/src/Factory/Api/Mastodon/Emoji.php index b7f4ee6ead..712bddb48d 100644 --- a/src/Factory/Api/Mastodon/Emoji.php +++ b/src/Factory/Api/Mastodon/Emoji.php @@ -34,19 +34,28 @@ class Emoji extends BaseFactory /** * Creates an emoji collection from shortcode => image mappings. * + * Only emojis with shortcodes of the form of ':shortcode:' are passed in the collection. + * * @param array $smilies + * @param bool $extract_url * * @return Emojis */ - public function createCollectionFromArray(array $smilies): Emojis + public function createCollectionFromArray(array $smilies, bool $extract_url = true): Emojis { $prototype = null; $emojis = []; - foreach ($smilies as $shortcode => $icon) { - if (preg_match('/src="(.+?)"/', $icon, $matches)) { - $url = $matches[1]; + foreach ($smilies as $shortcode => $url) { + if (substr($shortcode, 0, 1) == ':' && substr($shortcode, -1) == ':') { + if ($extract_url) { + if (preg_match('/src="(.+?)"/', $url, $matches)) { + $url = $matches[1]; + } else { + continue; + } + } $shortcode = trim($shortcode, ':'); if ($prototype === null) { diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index 4cdc8a78fc..fb73432f03 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -292,6 +292,10 @@ class Status extends BaseFactory if (DI::baseUrl()->isLocalUrl($item['uri'])) { $used_smilies = Smilies::extractUsedSmilies($item['body'] ?: $item['raw-body']); $emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true); + } else { + if (preg_match_all("(\[emoji=(.*?)](.*?)\[/emoji])ism", $item['body'] ?: $item['raw-body'], $matches)) { + $emojis = $this->mstdnEmojiFactory->createCollectionFromArray(array_combine($matches[2], $matches[1]), false)->getArrayCopy(true); + } } if ($is_reshare) { From d45e9d6af23b84c4ae0ddf96c112a0a01692dd8e Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Wed, 15 Nov 2023 23:53:38 +0800 Subject: [PATCH 5/6] Require whitespace around smilies and normalize federating text --- src/Content/Smilies.php | 167 ++++++++++++++---- src/Factory/Api/Mastodon/Emoji.php | 26 ++- src/Factory/Api/Mastodon/Status.php | 11 +- src/Protocol/ActivityPub/Transmitter.php | 16 +- tests/datasets/api.fixture.php | 33 ++++ tests/src/Content/SmiliesTest.php | 103 +++++++++++ tests/src/Factory/Api/Mastodon/EmojiTest.php | 45 +++++ tests/src/Factory/Api/Mastodon/StatusTest.php | 61 +++++++ .../Protocol/ActivityPub/TransmitterTest.php | 53 ++++++ 9 files changed, 458 insertions(+), 57 deletions(-) create mode 100644 tests/src/Factory/Api/Mastodon/EmojiTest.php create mode 100644 tests/src/Factory/Api/Mastodon/StatusTest.php create mode 100644 tests/src/Protocol/ActivityPub/TransmitterTest.php diff --git a/src/Content/Smilies.php b/src/Content/Smilies.php index 760bfbce9e..1aeab5b804 100644 --- a/src/Content/Smilies.php +++ b/src/Content/Smilies.php @@ -21,6 +21,7 @@ namespace Friendica\Content; +use Friendica\Content\Text\BBCode; use Friendica\Core\Hook; use Friendica\DI; use Friendica\Util\Strings; @@ -67,7 +68,7 @@ class Smilies */ public static function getList(): array { - $texts = [ + $texts = [ '<3', '</3', '<\\3', @@ -153,34 +154,129 @@ class Smilies } /** - * Finds all used smilies (like :heart: or :p) in the provided text. + * Normalizes smiley shortcodes into texts with no special symbols. * - * @param string $text that might contain smilie usages (denoted by a starting colon) - * @param bool $extract_url whether to further extract image urls - * @return array with smilie codes (colon included) as the keys, the smilie images as values + * @return array + * 'texts' => smilie shortcut + * 'icons' => icon url or an empty string + * 'norms' => normalized shortcut */ - public static function extractUsedSmilies(string $text, bool $extract_url = false): array + public static function getNormalizedList(): array + { + $smilies = self::getList(); + $norms = []; + $icons = $smilies['icons']; + foreach ($smilies['texts'] as $i => $shortcode) { + // Extract urls + $icon = $icons[$i]; + if (preg_match('/src="(.+?)"/', $icon, $match)) { + $icon = $match[1]; + } else { + $icon = ''; + } + $icons[$i] = $icon; + + // Normalize name + $norm = preg_replace('/[\s\-:#~]/', '', $shortcode); + if (ctype_alnum($norm)) { + $norms[] = $norm; + } elseif (preg_match('#/smiley-(\w+)\.gif#', $icon, $match)) { + $norms[] = $match[1]; + } else { + $norms[] = 'smiley' . $i; + } + } + $smilies['norms'] = $norms; + return $smilies; + } + + /** + * Finds all used smilies (denoted by quoting colons like :heart:) in the provided text and normalizes their usages. + * + * @param string $text that might contain smiley usages + * @return array with smilie codes (colon included) as the keys, their image urls as values; + * the normalized string is put under the '' (empty string) key + */ + public static function extractUsedSmilies(string $text): array { $emojis = []; - $smilies = self::getList(); - $icons = $smilies['icons']; - foreach ($smilies['texts'] as $i => $name) { - if (strstr($text, $name)) { - $image = $icons[$i]; - if ($extract_url) { - if (preg_match('/src="(.+?)"/', $image, $match)) { - $image = $match[1]; - } else { - continue; - } + $emojis[''] = BBCode::performWithEscapedTags($text, ['code'], function ($text) use (&$emojis) { + return BBCode::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use (&$emojis) { + if (strpos($text, '[nosmile]') !== false || self::noSmilies()) { + return $text; } - $emojis[$name] = $image; - } - } + $smilies = self::getNormalizedList(); + $normalized = array_combine($smilies['texts'], $smilies['norms']); + return self::performForEachWordMatch( + array_combine($smilies['texts'], $smilies['icons']), + $text, + function (string $name, string $image) use($normalized, &$emojis) { + $name = $normalized[$name]; + if (preg_match('/src="(.+?)"/', $image, $match)) { + $image = $match[1]; + $emojis[$name] = $image; + } + return ':' . $name . ':'; + }, + ); + }); + }); + return $emojis; } + /** + * Similar to strtr but matches only whole words and replaces texts with $callback. + * + * @param array $words + * @param string $subject + * @param callable $callback ($offset, $value) + * @return string + */ + private static function performForEachWordMatch(array $words, string $subject, callable $callback): string + { + $offset = 0; + $result = ''; + $processed = 0; + // Learned from PHP's strtr implementation + // Should probably improve performance once JIT-compiled + $length_bitset = 0; + $ord_bitset = 0; + foreach ($words as $word => $_) { + $length = strlen($word); + if ($length <= 31) { + $length_bitset |= 1 << $length; + } + $ord = ord($word); + $ord_bitset |= 1 << ($ord & 31); + } + + while ($offset < strlen($subject) && preg_match('/\s+?(?=\S|$)/', $subject, $matches, PREG_OFFSET_CAPTURE, $offset)) { + [$whitespaces, $next] = $matches[0]; + $word = substr($subject, $offset, $next - $offset); + + $shift = strlen($word); + $ord = ord($word); + if (($shift > 31 || ($length_bitset & (1 << $shift))) + && ($ord_bitset & (1 << ($ord & 31))) + && array_key_exists($word, $words)) { + $result .= substr($subject, $processed, $offset - $processed); + $result .= call_user_func($callback, $word, $words[$word]); + $processed = $offset + strlen($word); + } + $offset = $next + strlen($whitespaces); + } + $word = substr($subject, $offset); + if (array_key_exists($word, $words)) { + $result .= substr($subject, $processed, $offset - $processed); + $result .= call_user_func($callback, $word, $words[$word]); + } else { + $result .= substr($subject, $processed); + } + return $result; + } + /** * Copied from http://php.net/manual/en/function.str-replace.php#88569 * Modified for camel caps: renamed stro_replace -> strOrigReplace @@ -198,7 +294,13 @@ class Smilies */ private static function strOrigReplace(array $search, array $replace, string $subject): string { - return strtr($subject, array_combine($search, $replace)); + return self::performForEachWordMatch( + array_combine($search, $replace), + $subject, + function (string $_, string $value) { + return $value; + } + ); } /** @@ -227,6 +329,12 @@ class Smilies return $s; } + private static function noSmilies(): bool { + return (intval(DI::config()->get('system', 'no_smilies')) || + (DI::userSession()->getLocalUserId() && + intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'no_smilies')))); + } + /** * Replaces emoji shortcodes in a string from a structured array of searches and replaces. * @@ -240,9 +348,7 @@ class Smilies */ public static function replaceFromArray(string $text, array $smilies, bool $no_images = false): string { - if (intval(DI::config()->get('system', 'no_smilies')) - || (DI::userSession()->getLocalUserId() && intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'no_smilies'))) - ) { + if (self::noSmilies()) { return $text; } @@ -261,7 +367,7 @@ class Smilies $smilies = $cleaned; } - $text = preg_replace_callback('/<(3+)/', [self::class, 'heartReplaceCallback'], $text); + $text = preg_replace_callback('/\B<3+?\b/', [self::class, 'heartReplaceCallback'], $text); $text = self::strOrigReplace($smilies['texts'], $smilies['icons'], $text); $text = preg_replace_callback('/<(code)>(.*?)<\/code>/ism', [self::class, 'decode'], $text); @@ -302,16 +408,7 @@ class Smilies */ private static function heartReplaceCallback(array $matches): string { - if (strlen($matches[1]) == 1) { - return $matches[0]; - } - - $t = ''; - for ($cnt = 0; $cnt < strlen($matches[1]); $cnt ++) { - $t .= '❤'; - } - - return str_replace($matches[0], $t, $matches[0]); + return str_repeat('❤', strlen($matches[0]) - 4); } /** diff --git a/src/Factory/Api/Mastodon/Emoji.php b/src/Factory/Api/Mastodon/Emoji.php index 712bddb48d..0a2440426c 100644 --- a/src/Factory/Api/Mastodon/Emoji.php +++ b/src/Factory/Api/Mastodon/Emoji.php @@ -34,28 +34,18 @@ class Emoji extends BaseFactory /** * Creates an emoji collection from shortcode => image mappings. * - * Only emojis with shortcodes of the form of ':shortcode:' are passed in the collection. - * * @param array $smilies - * @param bool $extract_url * * @return Emojis */ - public function createCollectionFromArray(array $smilies, bool $extract_url = true): Emojis + public function createCollectionFromArray(array $smilies): Emojis { $prototype = null; $emojis = []; foreach ($smilies as $shortcode => $url) { - if (substr($shortcode, 0, 1) == ':' && substr($shortcode, -1) == ':') { - if ($extract_url) { - if (preg_match('/src="(.+?)"/', $url, $matches)) { - $url = $matches[1]; - } else { - continue; - } - } + if ($shortcode !== '' && $url !== '') { $shortcode = trim($shortcode, ':'); if ($prototype === null) { @@ -71,12 +61,20 @@ class Emoji extends BaseFactory } /** - * @param array $smilies + * @param array $smilies as is returned by Smilies::getList() * * @return Emojis */ public function createCollectionFromSmilies(array $smilies): Emojis { - return self::createCollectionFromArray(array_combine($smilies['texts'], $smilies['icons'])); + $emojis = []; + $icons = $smilies['icons']; + foreach ($smilies['texts'] as $i => $name) { + $url = $icons[$i]; + if (preg_match('/src="(.+?)"/', $url, $matches)) { + $emojis[$name] = $matches[1]; + } + } + return self::createCollectionFromArray($emojis); } } diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index fb73432f03..6d45b4d9fe 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -290,11 +290,18 @@ class Status extends BaseFactory $emojis = null; if (DI::baseUrl()->isLocalUrl($item['uri'])) { - $used_smilies = Smilies::extractUsedSmilies($item['body'] ?: $item['raw-body']); + $used_smilies = Smilies::extractUsedSmilies($item['raw-body'] ?: $item['body']); + // $used_smilies contains normalized texts + if ($item['raw-body']) { + $item['raw-body'] = $used_smilies['']; + } elseif ($item['body']) { + $item['body'] = $used_smilies['']; + } + unset($used_smilies['']); $emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true); } else { if (preg_match_all("(\[emoji=(.*?)](.*?)\[/emoji])ism", $item['body'] ?: $item['raw-body'], $matches)) { - $emojis = $this->mstdnEmojiFactory->createCollectionFromArray(array_combine($matches[2], $matches[1]), false)->getArrayCopy(true); + $emojis = $this->mstdnEmojiFactory->createCollectionFromArray(array_combine($matches[2], $matches[1]))->getArrayCopy(true); } } diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 130aa3ab08..56724e22d2 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -899,7 +899,7 @@ class Transmitter $tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BCC, Tag::AUDIENCE]); if (empty($tags)) { Logger::debug('No receivers found', ['uri-id' => $uri_id]); - $post = Post::selectFirst([Item::DELIVER_FIELDLIST], ['uri-id' => $uri_id, 'origin' => true]); + $post = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $uri_id, 'origin' => true]); if (!empty($post)) { ActivityPub\Transmitter::storeReceiversForItem($post); $tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BCC, Tag::AUDIENCE]); @@ -1512,10 +1512,14 @@ class Transmitter * * @param array $tags Tag array * @param string $text Text containing tags like :tag: + * @return string normalized text */ private static function addEmojiTags(array &$tags, string $text) { - foreach (Smilies::extractUsedSmilies($text, true) as $name => $url) { + $emojis = Smilies::extractUsedSmilies($text); + $normalized = $emojis['']; + unset($emojis['']); + foreach ($emojis as $name => $url) { $tags[] = [ 'type' => 'Emoji', 'name' => $name, @@ -1525,6 +1529,7 @@ class Transmitter ], ]; } + return $normalized; } /** @@ -1559,8 +1564,6 @@ class Transmitter } } - self::addEmojiTags($tags, $item['body']); - $announce = self::getAnnounceArray($item); // Mention the original author upon commented reshares if (!empty($announce['comment'])) { @@ -1808,10 +1811,11 @@ class Transmitter $item = Post\Media::addHTMLAttachmentToItem($item); $body = $item['body']; - + $emojis = []; if ($type == 'Note') { $body = $item['raw-body'] ?? self::removePictures($body); } + $body = self::addEmojiTags($emojis, $body); /** * @todo Improve the automated summary @@ -1893,7 +1897,7 @@ class Transmitter } $data['attachment'] = self::createAttachmentList($item); - $data['tag'] = self::createTagList($item, $data['quoteUrl'] ?? ''); + $data['tag'] = array_merge(self::createTagList($item, $data['quoteUrl'] ?? ''), $emojis); if (empty($data['location']) && (!empty($item['coord']) || !empty($item['location']))) { $data['location'] = self::createLocation($item); diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index f5b16f9c6e..876827d748 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -112,6 +112,11 @@ return [ 'uri' => 'http://localhost/profile/mutualcontact', 'guid' => '46', ], + [ + 'id' => 100, + 'uri' => 'https://friendica.local/posts/100', + 'guid' => '100', + ], ], 'contact' => [ [ @@ -363,6 +368,12 @@ return [ 'et sed beatae nihil ullam temporibus corporis ratione blanditiis', 'plink' => 'http://localhost/display/6', ], + [ + 'uri-id' => 100, + 'title' => 'item_title', + 'body' => ':like ~friendica no [code]:dislike[/code] :-p :-[', + 'plink' => 'https://friendica.local/post/100', + ], ], 'post' => [ [ @@ -744,6 +755,28 @@ return [ 'deleted' => 0, 'wall' => 0, ], + // An emoji post + [ + 'id' => 14, + 'uri-id' => 100, + 'visible' => 1, + 'contact-id' => 44, + 'author-id' => 44, + 'owner-id' => 42, + 'causer-id' => 44, + 'uid' => 0, + 'vid' => 8, + 'unseen' => 0, + 'parent-uri-id' => 7, + 'thr-parent-id' => 7, + 'private' => Item::PUBLIC, + 'global' => true, + 'gravity' => Item::GRAVITY_PARENT, + 'network' => Protocol::DFRN, + 'origin' => 0, + 'deleted' => 0, + 'wall' => 0, + ], ], 'post-thread' => [ [ diff --git a/tests/src/Content/SmiliesTest.php b/tests/src/Content/SmiliesTest.php index 38eb743e85..67ba313fe6 100644 --- a/tests/src/Content/SmiliesTest.php +++ b/tests/src/Content/SmiliesTest.php @@ -143,4 +143,107 @@ class SmiliesTest extends FixtureTest { $this->assertEquals($expected, Smilies::isEmojiPost($body)); } + + + public function dataReplace(): array + { + return [ + 'simple-1' => [ + 'expected' => 'alt=":-p"', + 'body' => ':-p', + ], + 'simple-1' => [ + 'expected' => 'alt=":-p"', + 'body' => ' :-p ', + ], + 'word-boundary-1' => [ + 'expected' => ':-pppp', + 'body' => ':-pppp', + ], + 'word-boundary-2' => [ + 'expected' => '~friendicaca', + 'body' => '~friendicaca', + ], + 'symbol-boundary-1' => [ + 'expected' => '(:-p)', + 'body' => '(:-p)', + ], + 'hearts-1' => [ + 'expected' => '❤ (❤) ❤', + 'body' => '<3 (<3) <3', + ], + 'hearts-8' => [ + 'expected' => '(❤❤❤❤❤❤❤❤)', + 'body' => '(<33333333)', + ], + 'no-hearts-1' => [ + 'expected' => '(<30)', + 'body' => '(<30)', + ], + 'no-hearts-2' => [ + 'expected' => '(3<33)', + 'body' => '(3<33)', + ], + ]; + } + + /** + * @dataProvider dataReplace + * + * @param string $expected + * @param string $body + */ + public function testReplace(string $expected, string $body) + { + $result = Smilies::replace($body); + $this->assertStringContainsString($expected, $result); + } + + public function dataExtractUsedSmilies(): array + { + return [ + 'single-smiley' => [ + 'expected' => ['like'], + 'body' => ':like', + 'normalized' => ':like:', + ], + 'multiple-smilies' => [ + 'expected' => ['like', 'dislike'], + 'body' => ':like :dislike', + 'normalized' => ':like: :dislike:', + ], + 'nosmile' => [ + 'expected' => [], + 'body' => '[nosmile] :like :like', + 'normalized' => '[nosmile] :like :like' + ], + 'in-code' => [ + 'expected' => [], + 'body' => '[code]:like :like :like[/code]', + 'normalized' => '[code]:like :like :like[/code]' + ], + '~friendica' => [ + 'expected' => ['friendica'], + 'body' => '~friendica', + 'normalized' => ':friendica:' + ], + ]; + } + + /** + * @dataProvider dataExtractUsedSmilies + * + * @param array $expected + * @param string $body + * @param stirng $normalized + */ + public function testExtractUsedSmilies(array $expected, string $body, string $normalized) + { + $extracted = Smilies::extractUsedSmilies($body); + $this->assertEquals($normalized, $extracted['']); + foreach ($expected as $shortcode) { + $this->assertArrayHasKey($shortcode, $extracted); + } + $this->assertEquals(count($expected), count($extracted) - 1); + } } diff --git a/tests/src/Factory/Api/Mastodon/EmojiTest.php b/tests/src/Factory/Api/Mastodon/EmojiTest.php new file mode 100644 index 0000000000..da67ea1639 --- /dev/null +++ b/tests/src/Factory/Api/Mastodon/EmojiTest.php @@ -0,0 +1,45 @@ +. + * + */ + +namespace Friendica\Test\src\Factory\Api\Mastodon; + +use Friendica\Content\Smilies; +use Friendica\DI; +use Friendica\Test\FixtureTest; + +class EmojiTest extends FixtureTest +{ + protected function setUp(): void + { + parent::setUp(); + + DI::config()->set('system', 'no_smilies', false); + } + + public function testBuiltInCollection() + { + $emoji = DI::mstdnEmoji(); + $collection = $emoji->createCollectionFromSmilies(Smilies::getList())->getArrayCopy(true); + foreach ($collection as $item) { + $this->assertTrue(preg_match('(/images/.*)', $item['url']) === 1, $item['url']); + } + } +} diff --git a/tests/src/Factory/Api/Mastodon/StatusTest.php b/tests/src/Factory/Api/Mastodon/StatusTest.php new file mode 100644 index 0000000000..7593d9a32b --- /dev/null +++ b/tests/src/Factory/Api/Mastodon/StatusTest.php @@ -0,0 +1,61 @@ +. + * + */ + +namespace Friendica\Test\src\Factory\Api\Mastodon; + +use Friendica\Model\Post; +use Friendica\DI; +use Friendica\Test\FixtureTest; + +class StatusTest extends FixtureTest +{ + protected $status; + + protected function setUp(): void + { + parent::setUp(); + + DI::config()->set('system', 'no_smilies', false); + $this->status = DI::mstdnStatus(); + } + + public function testSimpleStatus() + { + $post = Post::selectFirst([], ['id' => 13]); + $this->assertNotNull($post); + $result = $this->status->createFromUriId($post['uri-id']); + $this->assertNotNull($result); + } + + public function testSimpleEmojiStatus() + { + $post = Post::selectFirst([], ['id' => 14]); + $this->assertNotNull($post); + $result = $this->status->createFromUriId($post['uri-id'])->toArray(); + $this->assertEquals(':like: :friendica: no :dislike :p: :embarrassed:', $result['content']); + $emojis = array_fill_keys(['like', 'friendica', 'p', 'embarrassed'], true); + $this->assertEquals(count($emojis), count($result['emojis'])); + foreach ($result['emojis'] as $emoji) { + $this->assertTrue(array_key_exists($emoji['shortcode'], $emojis)); + $this->assertEquals(0, strpos($emoji['url'], 'http')); + } + } +} diff --git a/tests/src/Protocol/ActivityPub/TransmitterTest.php b/tests/src/Protocol/ActivityPub/TransmitterTest.php new file mode 100644 index 0000000000..c7a94bc598 --- /dev/null +++ b/tests/src/Protocol/ActivityPub/TransmitterTest.php @@ -0,0 +1,53 @@ +. + * + */ + +namespace Friendica\Test\src\Protocol\ActivityPub; + +use Friendica\DI; +use Friendica\Model\Post; +use Friendica\Protocol\ActivityPub\Transmitter; +use Friendica\Test\FixtureTest; + +class TransmitterTest extends FixtureTest +{ + protected function setUp(): void + { + parent::setUp(); + + DI::config()->set('system', 'no_smilies', false); + } + + public function testEmojiPost() + { + $post = Post::selectFirst([], ['id' => 14]); + $this->assertNotNull($post); + $note = Transmitter::createNote($post); + $this->assertNotNull($note); + + $this->assertEquals(':like: :friendica: no :dislike :p: :embarrassed:', $note['content']); + $emojis = array_fill_keys(['like', 'friendica', 'p', 'embarrassed'], true); + $this->assertEquals(count($emojis), count($note['tag'])); + foreach ($note['tag'] as $emoji) { + $this->assertTrue(array_key_exists($emoji['name'], $emojis)); + $this->assertEquals('Emoji', $emoji['type']); + } + } +} From d493946ba4411aab1d6c522378a703f29bfbcba7 Mon Sep 17 00:00:00 2001 From: gudzpoz Date: Thu, 16 Nov 2023 13:31:31 +0800 Subject: [PATCH 6/6] Allow using punctuation chars as smiley delimiters --- src/Content/Smilies.php | 96 ++++++++++++------- src/Factory/Api/Mastodon/Status.php | 8 +- src/Protocol/ActivityPub/Transmitter.php | 6 +- tests/datasets/api.fixture.php | 2 +- tests/src/Content/SmiliesTest.php | 33 +++++-- tests/src/Factory/Api/Mastodon/StatusTest.php | 2 +- .../Protocol/ActivityPub/TransmitterTest.php | 2 +- 7 files changed, 95 insertions(+), 54 deletions(-) diff --git a/src/Content/Smilies.php b/src/Content/Smilies.php index 1aeab5b804..9c357a9eb2 100644 --- a/src/Content/Smilies.php +++ b/src/Content/Smilies.php @@ -197,11 +197,11 @@ class Smilies * @return array with smilie codes (colon included) as the keys, their image urls as values; * the normalized string is put under the '' (empty string) key */ - public static function extractUsedSmilies(string $text): array + public static function extractUsedSmilies(string $text, string &$normalized = null): array { $emojis = []; - $emojis[''] = BBCode::performWithEscapedTags($text, ['code'], function ($text) use (&$emojis) { + $normalized = BBCode::performWithEscapedTags($text, ['code'], function ($text) use (&$emojis) { return BBCode::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use (&$emojis) { if (strpos($text, '[nosmile]') !== false || self::noSmilies()) { return $text; @@ -236,43 +236,69 @@ class Smilies */ private static function performForEachWordMatch(array $words, string $subject, callable $callback): string { - $offset = 0; + $ord1_bitset = 0; + $ord2_bitset = 0; + $prefixes = []; + foreach ($words as $word => $_) { + if (strlen($word) < 2 || !ctype_graph($word)) { + continue; + } + $ord1 = ord($word); + $ord2 = ord($word[1]); + $ord1_bitset |= 1 << ($ord1 & 31); + $ord2_bitset |= 1 << ($ord2 & 31); + if (!array_key_exists($word[0], $prefixes)) { + $prefixes[$word[0]] = []; + } + $prefixes[$word[0]][] = $word; + } + $result = ''; $processed = 0; - // Learned from PHP's strtr implementation - // Should probably improve performance once JIT-compiled - $length_bitset = 0; - $ord_bitset = 0; - foreach ($words as $word => $_) { - $length = strlen($word); - if ($length <= 31) { - $length_bitset |= 1 << $length; + $s_start = 0; // Segment start + // No spaces are allowed in smilies, so they can serve as delimiters. + // Splitting by some delimiters may not necessary though? + while (true) { + if ($s_start >= strlen($subject)) { + $result .= substr($subject, $processed); + break; } - $ord = ord($word); - $ord_bitset |= 1 << ($ord & 31); - } - - while ($offset < strlen($subject) && preg_match('/\s+?(?=\S|$)/', $subject, $matches, PREG_OFFSET_CAPTURE, $offset)) { - [$whitespaces, $next] = $matches[0]; - $word = substr($subject, $offset, $next - $offset); - - $shift = strlen($word); - $ord = ord($word); - if (($shift > 31 || ($length_bitset & (1 << $shift))) - && ($ord_bitset & (1 << ($ord & 31))) - && array_key_exists($word, $words)) { - $result .= substr($subject, $processed, $offset - $processed); - $result .= call_user_func($callback, $word, $words[$word]); - $processed = $offset + strlen($word); + if (preg_match('/\s+?(?=\S|$)/', $subject, $match, PREG_OFFSET_CAPTURE, $s_start)) { + [$whitespaces, $s_end] = $match[0]; + } else { + $s_end = strlen($subject); + $whitespaces = ''; } - $offset = $next + strlen($whitespaces); - } - $word = substr($subject, $offset); - if (array_key_exists($word, $words)) { - $result .= substr($subject, $processed, $offset - $processed); - $result .= call_user_func($callback, $word, $words[$word]); - } else { - $result .= substr($subject, $processed); + $s_length = $s_end - $s_start; + if ($s_length > 1) { + $segment = substr($subject, $s_start, $s_length); + // Find possible starting points for smilies. + // For built-in smilies, the two bitsets should make attempts quite efficient. + // However, presuming custom smilies follow the format of ":shortcode" or ":shortcode:", + // if the user adds more smilies (with addons), the second bitset may eventually become useless. + for ($i = 0; $i < $s_length - 1; $i++) { + $c = $segment[$i]; + $d = $segment[$i + 1]; + if (($ord1_bitset & (1 << (ord($c) & 31))) && ($ord2_bitset & (1 << (ord($d) & 31))) && array_key_exists($c, $prefixes)) { + foreach ($prefixes[$c] as $word) { + $wlength = strlen($word); + if ($wlength <= $s_length - $i && substr($segment, $i, $wlength) === $word) { + // Check for boundaries + if (($i === 0 || ctype_space($segment[$i - 1]) || ctype_punct($segment[$i - 1])) + && ($i + $wlength >= $s_length || ctype_space($segment[$i + $wlength]) || ctype_punct($segment[$i + $wlength]))) { + $result .= substr($subject, $processed, $s_start - $processed + $i); + $result .= call_user_func($callback, $word, $words[$word]); + $i += $wlength; + $processed = $s_start + $i; + $i--; + break; + } + } + } + } + } + } + $s_start = $s_end + strlen($whitespaces); } return $result; } diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index 6d45b4d9fe..5cd90ecb5f 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -290,14 +290,12 @@ class Status extends BaseFactory $emojis = null; if (DI::baseUrl()->isLocalUrl($item['uri'])) { - $used_smilies = Smilies::extractUsedSmilies($item['raw-body'] ?: $item['body']); - // $used_smilies contains normalized texts + $used_smilies = Smilies::extractUsedSmilies($item['raw-body'] ?: $item['body'], $normalized); if ($item['raw-body']) { - $item['raw-body'] = $used_smilies['']; + $item['raw-body'] = $normalized; } elseif ($item['body']) { - $item['body'] = $used_smilies['']; + $item['body'] = $normalized; } - unset($used_smilies['']); $emojis = $this->mstdnEmojiFactory->createCollectionFromArray($used_smilies)->getArrayCopy(true); } else { if (preg_match_all("(\[emoji=(.*?)](.*?)\[/emoji])ism", $item['body'] ?: $item['raw-body'], $matches)) { diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 56724e22d2..9d0d998f9d 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -1514,11 +1514,9 @@ class Transmitter * @param string $text Text containing tags like :tag: * @return string normalized text */ - private static function addEmojiTags(array &$tags, string $text) + private static function addEmojiTags(array &$tags, string $text): string { - $emojis = Smilies::extractUsedSmilies($text); - $normalized = $emojis['']; - unset($emojis['']); + $emojis = Smilies::extractUsedSmilies($text, $normalized); foreach ($emojis as $name => $url) { $tags[] = [ 'type' => 'Emoji', diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index 876827d748..2bf38a5e91 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -371,7 +371,7 @@ return [ [ 'uri-id' => 100, 'title' => 'item_title', - 'body' => ':like ~friendica no [code]:dislike[/code] :-p :-[', + 'body' => ':like ~friendica no [code]:dislike[/code] :-p :-[ <3', 'plink' => 'https://friendica.local/post/100', ], ], diff --git a/tests/src/Content/SmiliesTest.php b/tests/src/Content/SmiliesTest.php index 67ba313fe6..e41e59ca82 100644 --- a/tests/src/Content/SmiliesTest.php +++ b/tests/src/Content/SmiliesTest.php @@ -147,7 +147,7 @@ class SmiliesTest extends FixtureTest public function dataReplace(): array { - return [ + $data = [ 'simple-1' => [ 'expected' => 'alt=":-p"', 'body' => ':-p', @@ -165,7 +165,7 @@ class SmiliesTest extends FixtureTest 'body' => '~friendicaca', ], 'symbol-boundary-1' => [ - 'expected' => '(:-p)', + 'expected' => 'alt=":-p"', 'body' => '(:-p)', ], 'hearts-1' => [ @@ -185,6 +185,19 @@ class SmiliesTest extends FixtureTest 'body' => '(3<33)', ], ]; + foreach ([':-[', ':-D', 'o.O'] as $emoji) { + foreach (['A', '_', ':', '-'] as $prefix) { + foreach (['', ' ', 'A', ':', '-'] as $suffix) { + $no_smile = ($prefix !== '' && ctype_alnum($prefix)) || ($suffix !== '' && ctype_alnum($suffix)); + $s = $prefix . $emoji . $suffix; + $data[] = [ + 'expected' => $no_smile ? $s : 'alt="' . $emoji . '"', + 'body' => $s, + ]; + } + } + } + return $data; } /** @@ -202,6 +215,11 @@ class SmiliesTest extends FixtureTest public function dataExtractUsedSmilies(): array { return [ + 'symbols' => [ + 'expected' => ['p', 'heart', 'embarrassed', 'kiss'], + 'body' => ':-p <3 ":-[:-"', + 'normalized' => ':p: :heart: ":embarrassed::kiss:', + ], 'single-smiley' => [ 'expected' => ['like'], 'body' => ':like', @@ -239,11 +257,12 @@ class SmiliesTest extends FixtureTest */ public function testExtractUsedSmilies(array $expected, string $body, string $normalized) { - $extracted = Smilies::extractUsedSmilies($body); - $this->assertEquals($normalized, $extracted['']); - foreach ($expected as $shortcode) { - $this->assertArrayHasKey($shortcode, $extracted); + $extracted = Smilies::extractUsedSmilies($body, $converted); + $expected = array_fill_keys($expected, true); + $this->assertEquals($normalized, $converted); + foreach (array_keys($extracted) as $shortcode) { + $this->assertArrayHasKey($shortcode, $expected); } - $this->assertEquals(count($expected), count($extracted) - 1); + $this->assertEquals(count($expected), count($extracted)); } } diff --git a/tests/src/Factory/Api/Mastodon/StatusTest.php b/tests/src/Factory/Api/Mastodon/StatusTest.php index 7593d9a32b..d150d85574 100644 --- a/tests/src/Factory/Api/Mastodon/StatusTest.php +++ b/tests/src/Factory/Api/Mastodon/StatusTest.php @@ -50,7 +50,7 @@ class StatusTest extends FixtureTest $post = Post::selectFirst([], ['id' => 14]); $this->assertNotNull($post); $result = $this->status->createFromUriId($post['uri-id'])->toArray(); - $this->assertEquals(':like: :friendica: no :dislike :p: :embarrassed:', $result['content']); + $this->assertEquals(':like: :friendica: no :dislike :p: :embarrassed: ❤', $result['content']); $emojis = array_fill_keys(['like', 'friendica', 'p', 'embarrassed'], true); $this->assertEquals(count($emojis), count($result['emojis'])); foreach ($result['emojis'] as $emoji) { diff --git a/tests/src/Protocol/ActivityPub/TransmitterTest.php b/tests/src/Protocol/ActivityPub/TransmitterTest.php index c7a94bc598..49b51da4b9 100644 --- a/tests/src/Protocol/ActivityPub/TransmitterTest.php +++ b/tests/src/Protocol/ActivityPub/TransmitterTest.php @@ -42,7 +42,7 @@ class TransmitterTest extends FixtureTest $note = Transmitter::createNote($post); $this->assertNotNull($note); - $this->assertEquals(':like: :friendica: no :dislike :p: :embarrassed:', $note['content']); + $this->assertEquals(':like: :friendica: no :dislike :p: :embarrassed: ❤', $note['content']); $emojis = array_fill_keys(['like', 'friendica', 'p', 'embarrassed'], true); $this->assertEquals(count($emojis), count($note['tag'])); foreach ($note['tag'] as $emoji) {