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", '', $text);
+ if ($simple_html == self::MASTODON_API) {
+ $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text);
+ } else {
+ $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '', $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) {