X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModule%2FApi%2FMastodon%2FStatuses.php;h=87840eef9704b0e615758ea24beb95afc7266462;hb=bb2f678d6b2c1d5e2cddca49df005a51910d2b76;hp=afaf1598600ae13f36fc7337f9474a109e67fc65;hpb=6eb3874646a0e6c42c546047032606da68fb9848;p=friendica.git diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index afaf159860..87840eef97 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -1,6 +1,6 @@ getRequest([ + 'status' => '', // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided. + 'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used. + 'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply + 'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. + 'language' => '', // ISO 639 language code for this status. + 'friendica' => [], + ], $request); + + $owner = User::getOwnerDataById($uid); + + $condition = [ + 'uid' => $uid, + 'uri-id' => $this->parameters['id'], + 'contact-id' => $owner['id'], + 'author-id' => Contact::getPublicIdByUserId($uid), + 'origin' => true, + ]; + + $post = Post::selectFirst(['uri-id', 'id', 'gravity', 'uid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'network'], $condition); + if (empty($post['id'])) { + throw new HTTPException\NotFoundException('Item with URI ID ' . $this->parameters['id'] . ' not found for user ' . $uid . '.'); + } + + // The imput is defined as text. So we can use Markdown for some enhancements + $body = Markdown::toBBCode($request['status']); + + if (DI::pConfig()->get($uid, 'system', 'api_auto_attach', false) && preg_match("/\[url=[^\[\]]*\](.*)\[\/url\]\z/ism", $body, $matches)) { + $body = preg_replace("/\[url=[^\[\]]*\].*\[\/url\]\z/ism", PageInfo::getFooterFromUrl($matches[1]), $body); + } + + $item['title'] = ''; + $item['uid'] = $post['uid']; + $item['body'] = $body; + $item['network'] = $post['network']; + $item['app'] = $this->getApp(); + + if (!empty($request['language'])) { + $item['language'] = json_encode([$request['language'] => 1]); + } + + if ($post['gravity'] == Item::GRAVITY_PARENT) { + $item['title'] = $request['friendica']['title'] ?? ''; + } + + $spoiler_text = $request['spoiler_text']; + + if (!empty($spoiler_text)) { + if (!isset($request['friendica']['title']) && $post['gravity'] == Item::GRAVITY_PARENT && DI::pConfig()->get($uid, 'system', 'api_spoiler_title', true)) { + $item['title'] = $spoiler_text; + } else { + $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $spoiler_text . "[/abstract]\n" . $item['body']; + $item['content-warning'] = BBCode::toPlaintext($spoiler_text); + } + } + + $item = DI::contentItem()->expandTags($item, $request['visibility'] == 'direct'); + + if (!empty($request['media_ids'])) { + /* + The provided ids in the request value consists of these two sources: + - The id in the "photo" table for newly uploaded media + - The id in the "post-media" table for already attached media + + Because of this we have to add all media that isn't already attached. + Also we have to delete all media that isn't provided anymore. + + There is a possible situation where the newly uploaded media + could have the same id as an existing, but deleted media. + + We can't do anything about this, but the probability for this is extremely low. + */ + $media_ids = []; + $existing_media = array_column(Post\Media::getByURIId($post['uri-id'], [Post\Media::AUDIO, Post\Media::VIDEO, Post\Media::IMAGE]), 'id'); + + foreach ($request['media_ids'] as $media) { + if (!in_array($media, $existing_media)) { + $media_ids[] = $media; + } + } + + foreach ($existing_media as $media) { + if (!in_array($media, $request['media_ids'])) { + Post\Media::deleteById($media); + } + } + + $item = $this->storeMediaIds($media_ids, array_merge($post, $item)); + + foreach ($item['attachments'] as $attachment) { + $attachment['uri-id'] = $post['uri-id']; + Post\Media::insert($attachment); + } + unset($item['attachments']); + } + if (!Item::isValid($item)) { + throw new \Exception('Missing parameters in definition'); + } + + Item::update($item, ['id' => $post['id']]); + + foreach (Tag::getByURIId($post['uri-id']) as $tagToRemove) { + Tag::remove($post['uri-id'], $tagToRemove['type'], $tagToRemove['name'], $tagToRemove['url']); + } + // Store tags from the body if this hadn't been handled previously in the protocol classes + + Tag::storeFromBody($post['uri-id'], Item::setHashtags($item['body'])); + + Item::updateDisplayCache($post['uri-id']); + + System::jsonExit(DI::mstdnStatus()->createFromUriId($post['uri-id'], $uid, self::appSupportsQuotes())); } - public static function delete(array $parameters = []) + protected function post(array $request = []) { - self::unsupported('delete'); + self::checkAllowedScope(self::SCOPE_WRITE); + $uid = self::getCurrentUserID(); + + $request = $this->getRequest([ + 'status' => '', // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided. + 'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used. + 'poll' => [], // Poll data. If provided, media_ids cannot be used, and poll[expires_in] must be provided. + 'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply + 'quote_id' => 0, // ID of the message to quote + 'sensitive' => false, // Mark status and attached media as sensitive? + 'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. + 'visibility' => '', // Visibility of the posted status. One of: "public", "unlisted", "private" or "direct". + 'scheduled_at' => '', // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. + 'language' => '', // ISO 639 language code for this status. + 'friendica' => [], // Friendica extensions to the standard Mastodon API spec + ], $request); + + $owner = User::getOwnerDataById($uid); + + // The imput is defined as text. So we can use Markdown for some enhancements + $body = Markdown::toBBCode($request['status']); + + if (DI::pConfig()->get($uid, 'system', 'api_auto_attach', false) && preg_match("/\[url=[^\[\]]*\](.*)\[\/url\]\z/ism", $body, $matches)) { + $body = preg_replace("/\[url=[^\[\]]*\].*\[\/url\]\z/ism", PageInfo::getFooterFromUrl($matches[1]), $body); + } + + $item = []; + $item['network'] = Protocol::DFRN; + $item['uid'] = $uid; + $item['verb'] = Activity::POST; + $item['contact-id'] = $owner['id']; + $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); + $item['title'] = ''; + $item['body'] = $body; + $item['app'] = $this->getApp(); + + switch ($request['visibility']) { + case 'public': + $item['allow_cid'] = ''; + $item['allow_gid'] = ''; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + $item['private'] = Item::PUBLIC; + break; + case 'unlisted': + $item['allow_cid'] = ''; + $item['allow_gid'] = ''; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + $item['private'] = Item::UNLISTED; + break; + case 'private': + if (!empty($owner['allow_cid'] . $owner['allow_gid'] . $owner['deny_cid'] . $owner['deny_gid'])) { + $item['allow_cid'] = $owner['allow_cid']; + $item['allow_gid'] = $owner['allow_gid']; + $item['deny_cid'] = $owner['deny_cid']; + $item['deny_gid'] = $owner['deny_gid']; + } else { + $item['allow_cid'] = ''; + $item['allow_gid'] = '<' . Group::FOLLOWERS . '>'; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + } + $item['private'] = Item::PRIVATE; + break; + case 'direct': + $item['private'] = Item::PRIVATE; + // The permissions are assigned in "expandTags" + break; + default: + if (is_numeric($request['visibility']) && Group::exists($request['visibility'], $uid)) { + $item['allow_cid'] = ''; + $item['allow_gid'] = '<' . $request['visibility'] . '>'; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + } else { + $item['allow_cid'] = $owner['allow_cid']; + $item['allow_gid'] = $owner['allow_gid']; + $item['deny_cid'] = $owner['deny_cid']; + $item['deny_gid'] = $owner['deny_gid']; + } + + if (!empty($item['allow_cid'] . $item['allow_gid'] . $item['deny_cid'] . $item['deny_gid'])) { + $item['private'] = Item::PRIVATE; + } elseif (DI::pConfig()->get($uid, 'system', 'unlisted')) { + $item['private'] = Item::UNLISTED; + } else { + $item['private'] = Item::PUBLIC; + } + break; + } + + if (!empty($request['language'])) { + $item['language'] = json_encode([$request['language'] => 1]); + } + + if ($request['in_reply_to_id']) { + $parent = Post::selectFirst(['uri', 'private'], ['uri-id' => $request['in_reply_to_id'], 'uid' => [0, $uid]]); + + $item['thr-parent'] = $parent['uri']; + $item['gravity'] = Item::GRAVITY_COMMENT; + $item['object-type'] = Activity\ObjectType::COMMENT; + + if (in_array($parent['private'], [Item::UNLISTED, Item::PUBLIC]) && ($item['private'] == Item::PRIVATE)) { + throw new HTTPException\NotImplementedException('Private replies for public posts are not implemented.'); + } + } else { + self::checkThrottleLimit(); + + $item['gravity'] = Item::GRAVITY_PARENT; + $item['object-type'] = Activity\ObjectType::NOTE; + } + + if ($request['quote_id']) { + if (!Post::exists(['uri-id' => $request['quote_id'], 'uid' => [0, $uid]])) { + throw new HTTPException\NotFoundException('Item with URI ID ' . $request['quote_id'] . ' not found for user ' . $uid . '.'); + } + $item['quote-uri-id'] = $request['quote_id']; + } + + $item['title'] = $request['friendica']['title'] ?? ''; + + if (!empty($request['spoiler_text'])) { + if (!isset($request['friendica']['title']) && !$request['in_reply_to_id'] && DI::pConfig()->get($uid, 'system', 'api_spoiler_title', true)) { + $item['title'] = $request['spoiler_text']; + } else { + $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $request['spoiler_text'] . "[/abstract]\n" . $item['body']; + } + } + + $item = DI::contentItem()->expandTags($item, $request['visibility'] == 'direct'); + + if (!empty($request['media_ids'])) { + $item = $this->storeMediaIds($request['media_ids'], $item); + } + + if (!empty($request['scheduled_at'])) { + $item['guid'] = Item::guid($item, true); + $item['uri'] = Item::newURI($item['guid']); + $id = Post\Delayed::add($item['uri'], $item, Worker::PRIORITY_HIGH, Post\Delayed::PREPARED, DateTimeFormat::utc($request['scheduled_at'])); + if (empty($id)) { + DI::mstdnError()->InternalError(); + } + System::jsonExit(DI::mstdnScheduledStatus()->createFromDelayedPostId($id, $uid)->toArray()); + } + + $id = Item::insert($item, true); + if (!empty($id)) { + $item = Post::selectFirst(['uri-id'], ['id' => $id]); + if (!empty($item['uri-id'])) { + System::jsonExit(DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, self::appSupportsQuotes())); + } + } + + DI::mstdnError()->InternalError(); + } + + protected function delete(array $request = []) + { + self::checkAllowedScope(self::SCOPE_READ); + $uid = self::getCurrentUserID(); + + if (empty($this->parameters['id'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + $item = Post::selectFirstForUser($uid, ['id'], ['uri-id' => $this->parameters['id'], 'uid' => $uid]); + if (empty($item['id'])) { + DI::mstdnError()->RecordNotFound(); + } + + if (!Item::markForDeletionById($item['id'])) { + DI::mstdnError()->RecordNotFound(); + } + + System::jsonExit([]); } /** - * @param array $parameters * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function rawContent(array $parameters = []) + protected function rawContent(array $request = []) { - if (empty($parameters['id'])) { + $uid = self::getCurrentUserID(); + + if (empty($this->parameters['id'])) { DI::mstdnError()->UnprocessableEntity(); } - System::jsonExit(DI::mstdnStatus()->createFromUriId($parameters['id'], self::getCurrentUserID())); + System::jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), false)); + } + + private function getApp(): string + { + if (!empty(self::getCurrentApplication()['name'])) { + return self::getCurrentApplication()['name']; + } else { + return 'API'; + } + } + + /** + * Store provided media ids in the item array and adjust permissions + * + * @param array $media_ids + * @param array $item + * @return array + */ + private function storeMediaIds(array $media_ids, array $item): array + { + $item['object-type'] = Activity\ObjectType::IMAGE; + $item['post-type'] = Item::PT_IMAGE; + $item['attachments'] = []; + + foreach ($media_ids as $id) { + $media = DBA::toArray(DBA::p("SELECT `resource-id`, `scale`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo` + WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = ?) AND `photo`.`uid` = ? + ORDER BY `photo`.`width` DESC LIMIT 2", $id, $item['uid'])); + + if (empty($media)) { + continue; + } + + Photo::setPermissionForRessource($media[0]['resource-id'], $item['uid'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']); + + $ressources[] = $media[0]['resource-id']; + $phototypes = Images::supportedTypes(); + $ext = $phototypes[$media[0]['type']]; + + $attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'], + 'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext, + 'size' => $media[0]['datasize'], + 'name' => $media[0]['filename'] ?: $media[0]['resource-id'], + 'description' => $media[0]['desc'] ?? '', + 'width' => $media[0]['width'], + 'height' => $media[0]['height']]; + + if (count($media) > 1) { + $attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext; + $attachment['preview-width'] = $media[1]['width']; + $attachment['preview-height'] = $media[1]['height']; + } + $item['attachments'][] = $attachment; + } + return $item; } }