--- /dev/null
+<?php
+/**
+ * @copyright Copyright (C) 2010-2023, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Content\Post\Collection;
+
+use Friendica\BaseCollection;
+use Friendica\Content\Post\Entity;
+
+class PostMedias extends BaseCollection
+{
+ /**
+ * @param Entity\PostMedia[] $entities
+ * @param int|null $totalCount
+ */
+ public function __construct(array $entities = [], int $totalCount = null)
+ {
+ parent::__construct($entities, $totalCount);
+ }
+
+ /**
+ * @return Entity\PostMedia
+ */
+ public function current(): Entity\PostMedia
+ {
+ return parent::current();
+ }
+}
--- /dev/null
+<?php
+/**
+ * @copyright Copyright (C) 2010-2023, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Content\Post\Entity;
+
+use Friendica\BaseEntity;
+use Friendica\Network\Entity\MimeType;
+use Friendica\Util\Proxy;
+use Psr\Http\Message\UriInterface;
+
+
+/**
+ * @property-read int $id
+ * @property-read int $uriId
+ * @property-read ?int $activityUriId
+ * @property-read UriInterface $url
+ * @property-read int $type
+ * @property-read MimeType $mimetype
+ * @property-read ?int $width
+ * @property-read ?int $height
+ * @property-read ?int $size
+ * @property-read ?UriInterface $preview
+ * @property-read ?int $previewWidth
+ * @property-read ?int $previewHeight
+ * @property-read ?string $description
+ * @property-read ?string $name
+ * @property-read ?UriInterface $authorUrl
+ * @property-read ?string $authorName
+ * @property-read ?UriInterface $authorImage
+ * @property-read ?UriInterface $publisherUrl
+ * @property-read ?string $publisherName
+ * @property-read ?UriInterface $publisherImage
+ * @property-read ?string $blurhash
+ */
+class PostMedia extends BaseEntity
+{
+ const TYPE_UNKNOWN = 0;
+ const TYPE_IMAGE = 1;
+ const TYPE_VIDEO = 2;
+ const TYPE_AUDIO = 3;
+ const TYPE_TEXT = 4;
+ const TYPE_APPLICATION = 5;
+ const TYPE_TORRENT = 16;
+ const TYPE_HTML = 17;
+ const TYPE_XML = 18;
+ const TYPE_PLAIN = 19;
+ const TYPE_ACTIVITY = 20;
+ const TYPE_ACCOUNT = 21;
+ const TYPE_DOCUMENT = 128;
+
+ /** @var int */
+ protected $id;
+ /** @var int */
+ protected $uriId;
+ /** @var UriInterface */
+ protected $url;
+ /** @var int One of TYPE_* */
+ protected $type;
+ /** @var MimeType */
+ protected $mimetype;
+ /** @var ?int */
+ protected $activityUriId;
+ /** @var ?int In pixels */
+ protected $width;
+ /** @var ?int In pixels */
+ protected $height;
+ /** @var ?int In bytes */
+ protected $size;
+ /** @var ?UriInterface Preview URL */
+ protected $preview;
+ /** @var ?int In pixels */
+ protected $previewWidth;
+ /** @var ?int In pixels */
+ protected $previewHeight;
+ /** @var ?string Alternative text like for images */
+ protected $description;
+ /** @var ?string */
+ protected $name;
+ /** @var ?UriInterface */
+ protected $authorUrl;
+ /** @var ?string */
+ protected $authorName;
+ /** @var ?UriInterface Image URL */
+ protected $authorImage;
+ /** @var ?UriInterface */
+ protected $publisherUrl;
+ /** @var ?string */
+ protected $publisherName;
+ /** @var ?UriInterface Image URL */
+ protected $publisherImage;
+ /** @var ?string Blurhash string representation for images
+ * @see https://github.com/woltapp/blurhash
+ * @see https://blurha.sh/
+ */
+ protected $blurhash;
+
+ public function __construct(
+ int $uriId,
+ UriInterface $url,
+ int $type,
+ MimeType $mimetype,
+ ?int $activityUriId,
+ ?int $width = null,
+ ?int $height = null,
+ ?int $size = null,
+ ?UriInterface $preview = null,
+ ?int $previewWidth = null,
+ ?int $previewHeight = null,
+ ?string $description = null,
+ ?string $name = null,
+ ?UriInterface $authorUrl = null,
+ ?string $authorName = null,
+ ?UriInterface $authorImage = null,
+ ?UriInterface $publisherUrl = null,
+ ?string $publisherName = null,
+ ?UriInterface $publisherImage = null,
+ ?string $blurhash = null,
+ int $id = null
+ )
+ {
+ $this->uriId = $uriId;
+ $this->url = $url;
+ $this->type = $type;
+ $this->mimetype = $mimetype;
+ $this->activityUriId = $activityUriId;
+ $this->width = $width;
+ $this->height = $height;
+ $this->size = $size;
+ $this->preview = $preview;
+ $this->previewWidth = $previewWidth;
+ $this->previewHeight = $previewHeight;
+ $this->description = $description;
+ $this->name = $name;
+ $this->authorUrl = $authorUrl;
+ $this->authorName = $authorName;
+ $this->authorImage = $authorImage;
+ $this->publisherUrl = $publisherUrl;
+ $this->publisherName = $publisherName;
+ $this->publisherImage = $publisherImage;
+ $this->blurhash = $blurhash;
+ $this->id = $id;
+ }
+
+
+ /**
+ * Get media link for given media id
+ *
+ * @param string $size One of the Proxy::SIZE_* constants
+ * @return string media link
+ */
+ public function getPhotoPath(string $size = ''): string
+ {
+ $url = '/photo/media/';
+ switch ($size) {
+ case Proxy::SIZE_MICRO:
+ $url .= Proxy::PIXEL_MICRO . '/';
+ break;
+ case Proxy::SIZE_THUMB:
+ $url .= Proxy::PIXEL_THUMB . '/';
+ break;
+ case Proxy::SIZE_SMALL:
+ $url .= Proxy::PIXEL_SMALL . '/';
+ break;
+ case Proxy::SIZE_MEDIUM:
+ $url .= Proxy::PIXEL_MEDIUM . '/';
+ break;
+ case Proxy::SIZE_LARGE:
+ $url .= Proxy::PIXEL_LARGE . '/';
+ break;
+ }
+ return $url . $this->id;
+ }
+
+ /**
+ * Get preview path for given media id relative to the base URL
+ *
+ * @param string $size One of the Proxy::SIZE_* constants
+ * @return string preview link
+ */
+ public function getPreviewPath(string $size = ''): string
+ {
+ $url = '/photo/preview/';
+ switch ($size) {
+ case Proxy::SIZE_MICRO:
+ $url .= Proxy::PIXEL_MICRO . '/';
+ break;
+ case Proxy::SIZE_THUMB:
+ $url .= Proxy::PIXEL_THUMB . '/';
+ break;
+ case Proxy::SIZE_SMALL:
+ $url .= Proxy::PIXEL_SMALL . '/';
+ break;
+ case Proxy::SIZE_MEDIUM:
+ $url .= Proxy::PIXEL_MEDIUM . '/';
+ break;
+ case Proxy::SIZE_LARGE:
+ $url .= Proxy::PIXEL_LARGE . '/';
+ break;
+ }
+ return $url . $this->id;
+ }
+}
--- /dev/null
+<?php
+/**
+ * @copyright Copyright (C) 2010-2023, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Content\Post\Factory;
+
+use Friendica\BaseFactory;
+use Friendica\Capabilities\ICanCreateFromTableRow;
+use Friendica\Content\Post\Entity;
+use Friendica\Network;
+use GuzzleHttp\Psr7\Uri;
+use Psr\Log\LoggerInterface;
+use stdClass;
+
+class PostMedia extends BaseFactory implements ICanCreateFromTableRow
+{
+ /** @var Network\Factory\MimeType */
+ private $mimeTypeFactory;
+
+ public function __construct(Network\Factory\MimeType $mimeTypeFactory, LoggerInterface $logger)
+ {
+ parent::__construct($logger);
+
+ $this->mimeTypeFactory = $mimeTypeFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createFromTableRow(array $row)
+ {
+ return new Entity\PostMedia(
+ $row['uri-id'],
+ $row['url'] ? new Uri($row['url']) : null,
+ $row['type'],
+ $this->mimeTypeFactory->createFromContentType($row['mimetype']),
+ $row['media-uri-id'],
+ $row['width'],
+ $row['height'],
+ $row['size'],
+ $row['preview'] ? new Uri($row['preview']) : null,
+ $row['preview-width'],
+ $row['preview-height'],
+ $row['description'],
+ $row['name'],
+ $row['author-url'] ? new Uri($row['author-url']) : null,
+ $row['author-name'],
+ $row['author-image'] ? new Uri($row['author-image']) : null,
+ $row['publisher-url'] ? new Uri($row['publisher-url']) : null,
+ $row['publisher-name'],
+ $row['publisher-image'] ? new Uri($row['publisher-image']) : null,
+ $row['blurhash'],
+ $row['id']
+ );
+ }
+
+ public function createFromBlueskyImageEmbed(int $uriId, stdClass $image): Entity\PostMedia
+ {
+ return new Entity\PostMedia(
+ $uriId,
+ new Uri($image->fullsize),
+ Entity\PostMedia::TYPE_IMAGE,
+ new Network\Entity\MimeType('unkn', 'unkn'),
+ null,
+ null,
+ null,
+ null,
+ new Uri($image->thumb),
+ null,
+ null,
+ $image->alt,
+ );
+ }
+
+
+ public function createFromBlueskyExternalEmbed(int $uriId, stdClass $external): Entity\PostMedia
+ {
+ return new Entity\PostMedia(
+ $uriId,
+ new Uri($external->uri),
+ Entity\PostMedia::TYPE_HTML,
+ new Network\Entity\MimeType('text', 'html'),
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ $external->description,
+ $external->title
+ );
+ }
+
+ public function createFromAttachment(int $uriId, array $attachment)
+ {
+ $attachment['uri-id'] = $uriId;
+ return $this->createFromTableRow($attachment);
+ }
+}
--- /dev/null
+<?php
+/**
+ * @copyright Copyright (C) 2010-2023, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Content\Post\Repository;
+
+use Friendica\BaseCollection;
+use Friendica\BaseRepository;
+use Friendica\Content\Post\Collection;
+use Friendica\Content\Post\Entity;
+use Friendica\Content\Post\Factory;
+use Friendica\Database\Database;
+use Friendica\Util\Strings;
+use Psr\Log\LoggerInterface;
+
+class PostMedia extends BaseRepository
+{
+ protected static $table_name = 'post-media';
+
+ public function __construct(Database $database, LoggerInterface $logger, Factory\PostMedia $factory)
+ {
+ parent::__construct($database, $logger, $factory);
+ }
+
+ protected function _select(array $condition, array $params = []): BaseCollection
+ {
+ $rows = $this->db->selectToArray(static::$table_name, [], $condition, $params);
+
+ $Entities = new Collection\PostMedias();
+ foreach ($rows as $fields) {
+ $Entities[] = $this->factory->createFromTableRow($fields);
+ }
+
+ return $Entities;
+ }
+
+ public function selectOneById(int $postMediaId): Entity\PostMedia
+ {
+ return $this->_selectOne(['id' => $postMediaId]);
+ }
+
+ public function selectByUriId(int $uriId): Collection\PostMedias
+ {
+ return $this->_select(['uri-id' => $uriId]);
+ }
+
+ public function save(Entity\PostMedia $PostMedia): Entity\PostMedia
+ {
+ $fields = [
+ 'uri-id' => $PostMedia->uriId,
+ 'url' => $PostMedia->url->__toString(),
+ 'type' => $PostMedia->type,
+ 'mimetype' => $PostMedia->mimetype->__toString(),
+ 'height' => $PostMedia->height,
+ 'width' => $PostMedia->width,
+ 'size' => $PostMedia->size,
+ 'preview' => $PostMedia->preview ? $PostMedia->preview->__toString() : null,
+ 'preview-height' => $PostMedia->previewHeight,
+ 'preview-width' => $PostMedia->previewWidth,
+ 'description' => $PostMedia->description,
+ 'name' => $PostMedia->name,
+ 'author-url' => $PostMedia->authorUrl ? $PostMedia->authorUrl->__toString() : null,
+ 'author-name' => $PostMedia->authorName,
+ 'author-image' => $PostMedia->authorImage ? $PostMedia->authorImage->__toString() : null,
+ 'publisher-url' => $PostMedia->publisherUrl ? $PostMedia->publisherUrl->__toString() : null,
+ 'publisher-name' => $PostMedia->publisherName,
+ 'publisher-image' => $PostMedia->publisherImage ? $PostMedia->publisherImage->__toString() : null,
+ 'media-uri-id' => $PostMedia->activityUriId,
+ 'blurhash' => $PostMedia->blurhash,
+ ];
+
+ if ($PostMedia->id) {
+ $this->db->update(self::$table_name, $fields, ['id' => $PostMedia->id]);
+ } else {
+ $this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE);
+
+ $newPostMediaId = $this->db->lastInsertId();
+
+ $PostMedia = $this->selectOneById($newPostMediaId);
+ }
+
+ return $PostMedia;
+ }
+
+
+ /**
+ * Split the attachment media in the three segments "visual", "link" and "additional"
+ *
+ * @param int $uri_id URI id
+ * @param array $links list of links that shouldn't be added
+ * @param bool $has_media
+ * @return Collection\PostMedias[] Three collections in "visual", "link" and "additional" keys
+ */
+ public function splitAttachments(int $uri_id, array $links = [], bool $has_media = true): array
+ {
+ $attachments = [
+ 'visual' => new Collection\PostMedias(),
+ 'link' => new Collection\PostMedias(),
+ 'additional' => new Collection\PostMedias(),
+ ];
+
+ if (!$has_media) {
+ return $attachments;
+ }
+
+ $PostMedias = $this->selectByUriId($uri_id);
+ if (!count($PostMedias)) {
+ return $attachments;
+ }
+
+ $heights = [];
+ $selected = '';
+ $previews = [];
+
+ foreach ($PostMedias as $PostMedia) {
+ foreach ($links as $link) {
+ if (Strings::compareLink($link, $PostMedia->url)) {
+ continue 2;
+ }
+ }
+
+ // Avoid adding separate media entries for previews
+ foreach ($previews as $preview) {
+ if (Strings::compareLink($preview, $PostMedia->url)) {
+ continue 2;
+ }
+ }
+
+ // Currently these two types are ignored here.
+ // Posts are added differently and contacts are not displayed as attachments.
+ if (in_array($PostMedia->type, [Entity\PostMedia::TYPE_ACCOUNT, Entity\PostMedia::TYPE_ACTIVITY])) {
+ continue;
+ }
+
+ if (!empty($PostMedia->preview)) {
+ $previews[] = $PostMedia->preview;
+ }
+
+ //$PostMedia->filetype = $filetype;
+ //$PostMedia->subtype = $subtype;
+
+ if ($PostMedia->type == Entity\PostMedia::TYPE_HTML || ($PostMedia->mimetype->type == 'text' && $PostMedia->mimetype->subtype == 'html')) {
+ $attachments['link'][] = $PostMedia;
+ continue;
+ }
+
+ if (
+ in_array($PostMedia->type, [Entity\PostMedia::TYPE_AUDIO, Entity\PostMedia::TYPE_IMAGE]) ||
+ in_array($PostMedia->mimetype->type, ['audio', 'image'])
+ ) {
+ $attachments['visual'][] = $PostMedia;
+ } elseif (($PostMedia->type == Entity\PostMedia::TYPE_VIDEO) || ($PostMedia->mimetype->type == 'video')) {
+ if (!empty($PostMedia->height)) {
+ // Peertube videos are delivered in many different resolutions. We pick a moderate one.
+ // Since only Peertube provides a "height" parameter, this wouldn't be executed
+ // when someone for example on Mastodon was sharing multiple videos in a single post.
+ $heights[$PostMedia->height] = (string)$PostMedia->url;
+ $video[(string) $PostMedia->url] = $PostMedia;
+ } else {
+ $attachments['visual'][] = $PostMedia;
+ }
+ } else {
+ $attachments['additional'][] = $PostMedia;
+ }
+ }
+
+ if (!empty($heights)) {
+ ksort($heights);
+ foreach ($heights as $height => $url) {
+ if (empty($selected) || $height <= 480) {
+ $selected = $url;
+ }
+ }
+
+ if (!empty($selected)) {
+ $attachments['visual'][] = $video[$selected];
+ unset($video[$selected]);
+ foreach ($video as $element) {
+ $attachments['additional'][] = $element;
+ }
+ }
+ }
+
+ return $attachments;
+ }
+
+}