diff --git a/database.sql b/database.sql index 0dcf9afe8..d080b4158 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2021.06-dev (Siberian Iris) --- DB_UPDATE_VERSION 1417 +-- DB_UPDATE_VERSION 1418 -- ------------------------------------------ @@ -378,6 +378,7 @@ CREATE TABLE IF NOT EXISTS `application` ( `read` boolean COMMENT 'Read scope', `write` boolean COMMENT 'Write scope', `follow` boolean COMMENT 'Follow scope', + `push` boolean COMMENT 'Push scope', PRIMARY KEY(`id`), UNIQUE INDEX `client_id` (`client_id`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='OAuth application'; @@ -395,6 +396,7 @@ CREATE TABLE IF NOT EXISTS `application-token` ( `read` boolean COMMENT 'Read scope', `write` boolean COMMENT 'Write scope', `follow` boolean COMMENT 'Follow scope', + `push` boolean COMMENT 'Push scope', PRIMARY KEY(`application-id`,`uid`), INDEX `uid_id` (`uid`,`application-id`), FOREIGN KEY (`application-id`) REFERENCES `application` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, @@ -1525,7 +1527,8 @@ CREATE VIEW `application-view` AS SELECT `application-token`.`scopes` AS `scopes`, `application-token`.`read` AS `read`, `application-token`.`write` AS `write`, - `application-token`.`follow` AS `follow` + `application-token`.`follow` AS `follow`, + `application-token`.`push` AS `push` FROM `application-token` INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`; diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index d7ce5402d..7df171b86 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -85,6 +85,7 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`POST /api/v1/notifications/clear`](https://docs.joinmastodon.org/methods/notifications/) - [`POST /api/v1/notifications/:id/dismiss`](https://docs.joinmastodon.org/methods/notifications/) - [`GET /api/v1/preferences`](https://docs.joinmastodon.org/methods/accounts/preferences/) +- [`POST /api/v1/statuses`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) - [`DELETE /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/statuses/:id/context`](https://docs.joinmastodon.org/methods/statuses/) @@ -120,7 +121,6 @@ These emdpoints are planned to be implemented - [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity) - [`POST /api/v1/media`](https://docs.joinmastodon.org/methods/statuses/media/) - [`PUT /api/v1/media/:id`](https://docs.joinmastodon.org/methods/statuses/media/) -- [`POST /api/v1/statuses`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/timelines/direct`](https://docs.joinmastodon.org/methods/timelines/) ## Non supportable endpoints diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index af423a676..32cd818ca 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -2115,6 +2115,32 @@ class BBCode return array_unique($ret); } + /** + * Expand tags to URLs + * + * @param string $body + * @return string body with expanded tags + */ + public static function expandTags(string $body) + { + return preg_replace_callback("/([!#@])([^\^ \x0D\x0A,;:?\']*[^\^ \x0D\x0A,;:?!\'.])/", + function ($match) { + switch ($match[1]) { + case '!': + case '@': + $contact = Contact::getByURL($match[2]); + if (!empty($contact)) { + return $match[1] . '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'; + } else { + return $match[1] . $match[2]; + } + break; + case '#': + return $match[1] . '[url=' . 'https://' . DI::baseUrl() . '/search?tag=' . $match[2] . ']' . $match[2] . '[/url]'; + } + }, $body); + } + /** * Perform a custom function on a text after having escaped blocks enclosed in the provided tag list. * diff --git a/src/Factory/Api/Mastodon/Error.php b/src/Factory/Api/Mastodon/Error.php index 086cceef9..4ec8744bf 100644 --- a/src/Factory/Api/Mastodon/Error.php +++ b/src/Factory/Api/Mastodon/Error.php @@ -54,6 +54,15 @@ class Error extends BaseFactory System::jsonError(401, $errorobj->toArray()); } + public function Forbidden(string $error = '') + { + $error = $error ?: DI::l10n()->t('Token is not authorized with a valid user or is missing a required scope'); + $error_description = ''; + $errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description); + + System::jsonError(403, $errorobj->toArray()); + } + public function InternalError(string $error = '') { $error = $error ?: DI::l10n()->t('Internal Server Error'); diff --git a/src/Module/Api/Friendica/Events/Index.php b/src/Module/Api/Friendica/Events/Index.php index 08225682a..3efa1a919 100644 --- a/src/Module/Api/Friendica/Events/Index.php +++ b/src/Module/Api/Friendica/Events/Index.php @@ -35,7 +35,7 @@ class Index extends BaseApi { public static function rawContent(array $parameters = []) { - if (self::login() === false) { + if (self::login(self::SCOPE_READ) === false) { throw new HTTPException\ForbiddenException(); } diff --git a/src/Module/Api/Friendica/Profile/Show.php b/src/Module/Api/Friendica/Profile/Show.php index ae0d1a79d..e550f839c 100644 --- a/src/Module/Api/Friendica/Profile/Show.php +++ b/src/Module/Api/Friendica/Profile/Show.php @@ -37,7 +37,7 @@ class Show extends BaseApi { public static function rawContent(array $parameters = []) { - if (self::login() === false) { + if (self::login(self::SCOPE_READ) === false) { throw new HTTPException\ForbiddenException(); } diff --git a/src/Module/Api/Mastodon/Accounts/Block.php b/src/Module/Api/Mastodon/Accounts/Block.php index edbae8a1d..fa12daaf4 100644 --- a/src/Module/Api/Mastodon/Accounts/Block.php +++ b/src/Module/Api/Mastodon/Accounts/Block.php @@ -33,7 +33,7 @@ class Block extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Follow.php b/src/Module/Api/Mastodon/Accounts/Follow.php index 921fac69a..bdd21c737 100644 --- a/src/Module/Api/Mastodon/Accounts/Follow.php +++ b/src/Module/Api/Mastodon/Accounts/Follow.php @@ -33,7 +33,7 @@ class Follow extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { @@ -42,6 +42,6 @@ class Follow extends BaseApi $cid = Contact::follow($parameters['id'], self::getCurrentUserID()); - System::jsonExit(DI::mstdnRelationship()->createFromContactId($cid)->toArray()); + System::jsonExit(DI::mstdnRelationship()->createFromContactId($cid, $uid)->toArray()); } } diff --git a/src/Module/Api/Mastodon/Accounts/Followers.php b/src/Module/Api/Mastodon/Accounts/Followers.php index 7e082edbd..4f560776c 100644 --- a/src/Module/Api/Mastodon/Accounts/Followers.php +++ b/src/Module/Api/Mastodon/Accounts/Followers.php @@ -37,7 +37,7 @@ class Followers extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Following.php b/src/Module/Api/Mastodon/Accounts/Following.php index 87e9117ea..7edf4e987 100644 --- a/src/Module/Api/Mastodon/Accounts/Following.php +++ b/src/Module/Api/Mastodon/Accounts/Following.php @@ -37,7 +37,7 @@ class Following extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/IdentityProofs.php b/src/Module/Api/Mastodon/Accounts/IdentityProofs.php index 5a7d3a840..f92de6cad 100644 --- a/src/Module/Api/Mastodon/Accounts/IdentityProofs.php +++ b/src/Module/Api/Mastodon/Accounts/IdentityProofs.php @@ -35,7 +35,7 @@ class IdentityProofs extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); System::jsonExit([]); } diff --git a/src/Module/Api/Mastodon/Accounts/Lists.php b/src/Module/Api/Mastodon/Accounts/Lists.php index 6ba7953ab..6a3e87b42 100644 --- a/src/Module/Api/Mastodon/Accounts/Lists.php +++ b/src/Module/Api/Mastodon/Accounts/Lists.php @@ -38,7 +38,7 @@ class Lists extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Mute.php b/src/Module/Api/Mastodon/Accounts/Mute.php index e3975771f..19413bacf 100644 --- a/src/Module/Api/Mastodon/Accounts/Mute.php +++ b/src/Module/Api/Mastodon/Accounts/Mute.php @@ -33,7 +33,7 @@ class Mute extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Note.php b/src/Module/Api/Mastodon/Accounts/Note.php index bbb53d93a..1f3dd8d91 100644 --- a/src/Module/Api/Mastodon/Accounts/Note.php +++ b/src/Module/Api/Mastodon/Accounts/Note.php @@ -34,7 +34,7 @@ class Note extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Relationships.php b/src/Module/Api/Mastodon/Accounts/Relationships.php index a989460a3..c134adf48 100644 --- a/src/Module/Api/Mastodon/Accounts/Relationships.php +++ b/src/Module/Api/Mastodon/Accounts/Relationships.php @@ -37,7 +37,7 @@ class Relationships extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($_REQUEST['id']) || !is_array($_REQUEST['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Search.php b/src/Module/Api/Mastodon/Accounts/Search.php index 77906661d..9f1d0aadf 100644 --- a/src/Module/Api/Mastodon/Accounts/Search.php +++ b/src/Module/Api/Mastodon/Accounts/Search.php @@ -40,7 +40,7 @@ class Search extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); // What to search for diff --git a/src/Module/Api/Mastodon/Accounts/Unblock.php b/src/Module/Api/Mastodon/Accounts/Unblock.php index 7de5a4cfb..14152c458 100644 --- a/src/Module/Api/Mastodon/Accounts/Unblock.php +++ b/src/Module/Api/Mastodon/Accounts/Unblock.php @@ -33,7 +33,7 @@ class Unblock extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Unfollow.php b/src/Module/Api/Mastodon/Accounts/Unfollow.php index b2efde7b0..2d00ea455 100644 --- a/src/Module/Api/Mastodon/Accounts/Unfollow.php +++ b/src/Module/Api/Mastodon/Accounts/Unfollow.php @@ -33,7 +33,7 @@ class Unfollow extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/Unmute.php b/src/Module/Api/Mastodon/Accounts/Unmute.php index 15892cff4..4b8111956 100644 --- a/src/Module/Api/Mastodon/Accounts/Unmute.php +++ b/src/Module/Api/Mastodon/Accounts/Unmute.php @@ -33,7 +33,7 @@ class Unmute extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php b/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php index 30b190a33..d3f4c15fe 100644 --- a/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php +++ b/src/Module/Api/Mastodon/Accounts/UpdateCredentials.php @@ -31,7 +31,7 @@ class UpdateCredentials extends BaseApi { public static function patch(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); $data = Network::postdata(); diff --git a/src/Module/Api/Mastodon/Accounts/VerifyCredentials.php b/src/Module/Api/Mastodon/Accounts/VerifyCredentials.php index facae7259..84945e1e2 100644 --- a/src/Module/Api/Mastodon/Accounts/VerifyCredentials.php +++ b/src/Module/Api/Mastodon/Accounts/VerifyCredentials.php @@ -38,7 +38,7 @@ class VerifyCredentials extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); $self = User::getOwnerDataById($uid); diff --git a/src/Module/Api/Mastodon/Announcements.php b/src/Module/Api/Mastodon/Announcements.php index 80b25bf5c..e9445c127 100644 --- a/src/Module/Api/Mastodon/Announcements.php +++ b/src/Module/Api/Mastodon/Announcements.php @@ -35,7 +35,7 @@ class Announcements extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); // @todo Possibly use the message from the pageheader addon for this System::jsonExit([]); diff --git a/src/Module/Api/Mastodon/Apps.php b/src/Module/Api/Mastodon/Apps.php index 8205691dc..c22225160 100644 --- a/src/Module/Api/Mastodon/Apps.php +++ b/src/Module/Api/Mastodon/Apps.php @@ -67,9 +67,10 @@ class Apps extends BaseApi $fields['scopes'] = $scopes; } - $fields['read'] = (stripos($scopes, 'read') !== false); - $fields['write'] = (stripos($scopes, 'write') !== false); - $fields['follow'] = (stripos($scopes, 'follow') !== false); + $fields['read'] = (stripos($scopes, self::SCOPE_READ) !== false); + $fields['write'] = (stripos($scopes, self::SCOPE_WRITE) !== false); + $fields['follow'] = (stripos($scopes, self::SCOPE_FOLLOW) !== false); + $fields['push'] = (stripos($scopes, self::SCOPE_PUSH) !== false); if (!empty($website)) { $fields['website'] = $website; diff --git a/src/Module/Api/Mastodon/Blocks.php b/src/Module/Api/Mastodon/Blocks.php index e93743ac2..b05cb313e 100644 --- a/src/Module/Api/Mastodon/Blocks.php +++ b/src/Module/Api/Mastodon/Blocks.php @@ -37,7 +37,7 @@ class Blocks extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Bookmarks.php b/src/Module/Api/Mastodon/Bookmarks.php index 8b03c40eb..88de40d69 100644 --- a/src/Module/Api/Mastodon/Bookmarks.php +++ b/src/Module/Api/Mastodon/Bookmarks.php @@ -39,7 +39,7 @@ class Bookmarks extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); // Maximum number of results to return. Defaults to 20. diff --git a/src/Module/Api/Mastodon/Favourited.php b/src/Module/Api/Mastodon/Favourited.php index 53bd82aab..1c0cf0a34 100644 --- a/src/Module/Api/Mastodon/Favourited.php +++ b/src/Module/Api/Mastodon/Favourited.php @@ -40,7 +40,7 @@ class Favourited extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); // Maximum number of results to return. Defaults to 20. diff --git a/src/Module/Api/Mastodon/Filters.php b/src/Module/Api/Mastodon/Filters.php new file mode 100644 index 000000000..cdcefc97f --- /dev/null +++ b/src/Module/Api/Mastodon/Filters.php @@ -0,0 +1,40 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\Core\System; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/filters/ + */ +class Filters extends BaseApi +{ + /** + * @param array $parameters + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function rawContent(array $parameters = []) + { + System::jsonError(404, ['error' => 'Record not found']); + } +} diff --git a/src/Module/Api/Mastodon/FollowRequests.php b/src/Module/Api/Mastodon/FollowRequests.php index 8a59120d3..3c66b4e6c 100644 --- a/src/Module/Api/Mastodon/FollowRequests.php +++ b/src/Module/Api/Mastodon/FollowRequests.php @@ -45,7 +45,7 @@ class FollowRequests extends BaseApi */ public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_FOLLOW); $uid = self::getCurrentUserID(); $introduction = DI::intro()->selectFirst(['id' => $parameters['id'], 'uid' => $uid]); @@ -83,7 +83,7 @@ class FollowRequests extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); $min_id = $_GET['min_id'] ?? null; diff --git a/src/Module/Api/Mastodon/Lists.php b/src/Module/Api/Mastodon/Lists.php index e655edcb2..496550d4a 100644 --- a/src/Module/Api/Mastodon/Lists.php +++ b/src/Module/Api/Mastodon/Lists.php @@ -33,7 +33,7 @@ class Lists extends BaseApi { public static function delete(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); @@ -54,7 +54,7 @@ class Lists extends BaseApi public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); $title = $_REQUEST['title'] ?? ''; @@ -90,7 +90,7 @@ class Lists extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Lists/Accounts.php b/src/Module/Api/Mastodon/Lists/Accounts.php index cb2bd208f..1ca2c8359 100644 --- a/src/Module/Api/Mastodon/Lists/Accounts.php +++ b/src/Module/Api/Mastodon/Lists/Accounts.php @@ -49,7 +49,7 @@ class Accounts extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Markers.php b/src/Module/Api/Mastodon/Markers.php index 45ad3927d..dc2949017 100644 --- a/src/Module/Api/Mastodon/Markers.php +++ b/src/Module/Api/Mastodon/Markers.php @@ -31,6 +31,8 @@ class Markers extends BaseApi { public static function post(array $parameters = []) { + self::login(self::SCOPE_WRITE); + self::unsupported('post'); } @@ -40,7 +42,7 @@ class Markers extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); System::jsonExit([]); } diff --git a/src/Module/Api/Mastodon/Media.php b/src/Module/Api/Mastodon/Media.php index 82844a6ff..55b8438c2 100644 --- a/src/Module/Api/Mastodon/Media.php +++ b/src/Module/Api/Mastodon/Media.php @@ -33,6 +33,9 @@ class Media extends BaseApi { public static function put(array $parameters = []) { + self::login(self::SCOPE_WRITE); + $uid = self::getCurrentUserID(); + $data = self::getPutData(); self::unsupported('put'); } @@ -43,7 +46,7 @@ class Media extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Mutes.php b/src/Module/Api/Mastodon/Mutes.php index d49bdad68..9e53da504 100644 --- a/src/Module/Api/Mastodon/Mutes.php +++ b/src/Module/Api/Mastodon/Mutes.php @@ -37,7 +37,7 @@ class Mutes extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Notifications.php b/src/Module/Api/Mastodon/Notifications.php index e01951722..30e1060d8 100644 --- a/src/Module/Api/Mastodon/Notifications.php +++ b/src/Module/Api/Mastodon/Notifications.php @@ -39,7 +39,7 @@ class Notifications extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (!empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Notifications/Clear.php b/src/Module/Api/Mastodon/Notifications/Clear.php index 5e0c53da0..c809ad2af 100644 --- a/src/Module/Api/Mastodon/Notifications/Clear.php +++ b/src/Module/Api/Mastodon/Notifications/Clear.php @@ -32,7 +32,7 @@ class Clear extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); DBA::update('notify', ['seen' => true], ['uid' => $uid]); diff --git a/src/Module/Api/Mastodon/Notifications/Dismiss.php b/src/Module/Api/Mastodon/Notifications/Dismiss.php index e8faa3096..a0f57a405 100644 --- a/src/Module/Api/Mastodon/Notifications/Dismiss.php +++ b/src/Module/Api/Mastodon/Notifications/Dismiss.php @@ -33,7 +33,7 @@ class Dismiss extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Preferences.php b/src/Module/Api/Mastodon/Preferences.php index 035357f07..f6eef4c79 100644 --- a/src/Module/Api/Mastodon/Preferences.php +++ b/src/Module/Api/Mastodon/Preferences.php @@ -37,7 +37,7 @@ class Preferences extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); $user = User::getById($uid, ['language', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']); diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 974c58bd3..e40709fe5 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -21,12 +21,19 @@ namespace Friendica\Module\Api\Mastodon; -use Friendica\Core\Logger; +use Friendica\Content\Text\BBCode; +use Friendica\Content\Text\Markdown; use Friendica\Core\System; +use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Model\Group; use Friendica\Model\Item; use Friendica\Model\Post; +use Friendica\Model\User; use Friendica\Module\BaseApi; +use Friendica\Protocol\Activity; +use Friendica\Util\Images; /** * @see https://docs.joinmastodon.org/methods/statuses/ @@ -35,13 +42,155 @@ class Statuses extends BaseApi { public static function post(array $parameters = []) { + self::login(self::SCOPE_WRITE); + $uid = self::getCurrentUserID(); + $data = self::getJsonPostData(); - self::unsupported('post'); + + $status = $data['status'] ?? ''; + $media_ids = $data['media_ids'] ?? []; + $in_reply_to_id = $data['in_reply_to_id'] ?? 0; + $sensitive = $data['sensitive'] ?? false; // @todo Possibly trigger "nsfw" flag? + $spoiler_text = $data['spoiler_text'] ?? ''; + $visibility = $data['visibility'] ?? ''; + $scheduled_at = $data['scheduled_at'] ?? ''; // Currently unsupported, but maybe in the future + $language = $data['language'] ?? ''; + + $owner = User::getOwnerDataById($uid); + + // The imput is defined as text. So we can use Markdown for some enhancements + $body = Markdown::toBBCode($status); + + $body = BBCode::expandTags($body); + + $item = []; + $item['uid'] = $uid; + $item['verb'] = Activity::POST; + $item['contact-id'] = $owner['id']; + $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); + $item['title'] = $spoiler_text; + $item['body'] = $body; + + if (!empty(self::getCurrentApplication()['name'])) { + $item['app'] = self::getCurrentApplication()['name']; + } + + if (empty($item['app'])) { + $item['app'] = 'API'; + } + + switch ($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': + // Direct messages are currently unsupported + DI::mstdnError()->InternalError('Direct messages are currently unsupported'); + break; + default: + $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($language)) { + $item['language'] = json_encode([$language => 1]); + } + + if ($in_reply_to_id) { + $parent = Post::selectFirst(['uri'], ['uri-id' => $in_reply_to_id, 'uid' => [0, $uid]]); + $item['thr-parent'] = $parent['uri']; + $item['gravity'] = GRAVITY_COMMENT; + $item['object-type'] = Activity\ObjectType::COMMENT; + } else { + $item['gravity'] = GRAVITY_PARENT; + $item['object-type'] = Activity\ObjectType::NOTE; + } + + if (!empty($media_ids)) { + $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, $uid)); + + if (empty($media)) { + continue; + } + + $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; + } + } + + $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)); + } + } + + DI::mstdnError()->InternalError(); } public static function delete(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Bookmark.php b/src/Module/Api/Mastodon/Statuses/Bookmark.php index acdf207a3..cd59337c3 100644 --- a/src/Module/Api/Mastodon/Statuses/Bookmark.php +++ b/src/Module/Api/Mastodon/Statuses/Bookmark.php @@ -35,7 +35,7 @@ class Bookmark extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Favourite.php b/src/Module/Api/Mastodon/Statuses/Favourite.php index 74892d960..006e06617 100644 --- a/src/Module/Api/Mastodon/Statuses/Favourite.php +++ b/src/Module/Api/Mastodon/Statuses/Favourite.php @@ -35,7 +35,7 @@ class Favourite extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Mute.php b/src/Module/Api/Mastodon/Statuses/Mute.php index 6a5d16d14..ff724ed53 100644 --- a/src/Module/Api/Mastodon/Statuses/Mute.php +++ b/src/Module/Api/Mastodon/Statuses/Mute.php @@ -34,7 +34,7 @@ class Mute extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Pin.php b/src/Module/Api/Mastodon/Statuses/Pin.php index 263780736..43cc0e0cd 100644 --- a/src/Module/Api/Mastodon/Statuses/Pin.php +++ b/src/Module/Api/Mastodon/Statuses/Pin.php @@ -34,7 +34,7 @@ class Pin extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Reblog.php b/src/Module/Api/Mastodon/Statuses/Reblog.php index cdd788dcf..d0194a312 100644 --- a/src/Module/Api/Mastodon/Statuses/Reblog.php +++ b/src/Module/Api/Mastodon/Statuses/Reblog.php @@ -37,7 +37,7 @@ class Reblog extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Unbookmark.php b/src/Module/Api/Mastodon/Statuses/Unbookmark.php index dd78d8833..bf8a3c6e2 100644 --- a/src/Module/Api/Mastodon/Statuses/Unbookmark.php +++ b/src/Module/Api/Mastodon/Statuses/Unbookmark.php @@ -35,7 +35,7 @@ class Unbookmark extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Unfavourite.php b/src/Module/Api/Mastodon/Statuses/Unfavourite.php index 528ef692e..72efdc0a7 100644 --- a/src/Module/Api/Mastodon/Statuses/Unfavourite.php +++ b/src/Module/Api/Mastodon/Statuses/Unfavourite.php @@ -35,7 +35,7 @@ class Unfavourite extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Unmute.php b/src/Module/Api/Mastodon/Statuses/Unmute.php index 26843be6c..531fe6235 100644 --- a/src/Module/Api/Mastodon/Statuses/Unmute.php +++ b/src/Module/Api/Mastodon/Statuses/Unmute.php @@ -34,7 +34,7 @@ class Unmute extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Unpin.php b/src/Module/Api/Mastodon/Statuses/Unpin.php index d16bfc33e..874be0cc3 100644 --- a/src/Module/Api/Mastodon/Statuses/Unpin.php +++ b/src/Module/Api/Mastodon/Statuses/Unpin.php @@ -34,7 +34,7 @@ class Unpin extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Statuses/Unreblog.php b/src/Module/Api/Mastodon/Statuses/Unreblog.php index 1296b7225..259ecfd2f 100644 --- a/src/Module/Api/Mastodon/Statuses/Unreblog.php +++ b/src/Module/Api/Mastodon/Statuses/Unreblog.php @@ -37,7 +37,7 @@ class Unreblog extends BaseApi { public static function post(array $parameters = []) { - self::login(); + self::login(self::SCOPE_WRITE); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Suggestions.php b/src/Module/Api/Mastodon/Suggestions.php index 97cfa11d3..df434d2dc 100644 --- a/src/Module/Api/Mastodon/Suggestions.php +++ b/src/Module/Api/Mastodon/Suggestions.php @@ -37,7 +37,7 @@ class Suggestions extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); // Maximum number of results to return. Defaults to 40. diff --git a/src/Module/Api/Mastodon/Timelines/Home.php b/src/Module/Api/Mastodon/Timelines/Home.php index c46c485fa..9c8cb7a0b 100644 --- a/src/Module/Api/Mastodon/Timelines/Home.php +++ b/src/Module/Api/Mastodon/Timelines/Home.php @@ -39,7 +39,7 @@ class Home extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); // Return results older than id diff --git a/src/Module/Api/Mastodon/Timelines/ListTimeline.php b/src/Module/Api/Mastodon/Timelines/ListTimeline.php index 7da16d137..f046cab78 100644 --- a/src/Module/Api/Mastodon/Timelines/ListTimeline.php +++ b/src/Module/Api/Mastodon/Timelines/ListTimeline.php @@ -39,7 +39,7 @@ class ListTimeline extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['id'])) { diff --git a/src/Module/Api/Mastodon/Timelines/Tag.php b/src/Module/Api/Mastodon/Timelines/Tag.php index 924b76307..b5d0d2140 100644 --- a/src/Module/Api/Mastodon/Timelines/Tag.php +++ b/src/Module/Api/Mastodon/Timelines/Tag.php @@ -40,7 +40,7 @@ class Tag extends BaseApi */ public static function rawContent(array $parameters = []) { - self::login(); + self::login(self::SCOPE_READ); $uid = self::getCurrentUserID(); if (empty($parameters['hashtag'])) { diff --git a/src/Module/Api/Twitter/ContactEndpoint.php b/src/Module/Api/Twitter/ContactEndpoint.php index 0e386bf93..3231d8b13 100644 --- a/src/Module/Api/Twitter/ContactEndpoint.php +++ b/src/Module/Api/Twitter/ContactEndpoint.php @@ -39,7 +39,7 @@ abstract class ContactEndpoint extends BaseApi { parent::init($parameters); - if (!self::login()) { + if (!self::login(self::SCOPE_READ)) { throw new HTTPException\UnauthorizedException(); } } diff --git a/src/Module/BaseApi.php b/src/Module/BaseApi.php index f826f4ad2..db4531d91 100644 --- a/src/Module/BaseApi.php +++ b/src/Module/BaseApi.php @@ -35,6 +35,11 @@ require_once __DIR__ . '/../../include/api.php'; class BaseApi extends BaseModule { + const SCOPE_READ = 'read'; + const SCOPE_WRITE = 'write'; + const SCOPE_FOLLOW = 'follow'; + const SCOPE_PUSH = 'push'; + /** * @var string json|xml|rss|atom */ @@ -43,6 +48,10 @@ class BaseApi extends BaseModule * @var bool|int */ protected static $current_user_id; + /** + * @var array + */ + protected static $current_token = []; public static function init(array $parameters = []) { @@ -171,6 +180,8 @@ class BaseApi extends BaseModule * * Simple Auth allow username in form of
user@server, ignoring server part * + * @param string $scope the requested scope (read, write, follow) + * * @return bool Was a user authenticated? * @throws HTTPException\ForbiddenException * @throws HTTPException\UnauthorizedException @@ -182,10 +193,22 @@ class BaseApi extends BaseModule * 'authenticated' => return status, * 'user_record' => return authenticated user record */ - protected static function login() + protected static function login(string $scope) { if (empty(self::$current_user_id)) { - self::$current_user_id = self::getUserByBearer(); + self::$current_token = self::getTokenByBearer(); + if (!empty(self::$current_token['uid'])) { + self::$current_user_id = self::$current_token['uid']; + } else { + self::$current_user_id = 0; + } + } + + if (!empty($scope) && !empty(self::$current_token)) { + if (empty(self::$current_token[$scope])) { + Logger::warning('The requested scope is not allowed', ['scope' => $scope, 'application' => self::$current_token]); + DI::mstdnError()->Forbidden(); + } } if (empty(self::$current_user_id)) { @@ -198,6 +221,16 @@ class BaseApi extends BaseModule return (bool)self::$current_user_id; } + /** + * Get current application + * + * @return array token + */ + protected static function getCurrentApplication() + { + return self::$current_token; + } + /** * Get current user id, returns 0 if not logged in * @@ -206,7 +239,13 @@ class BaseApi extends BaseModule protected static function getCurrentUserID() { if (empty(self::$current_user_id)) { - self::$current_user_id = self::getUserByBearer(); + self::$current_token = self::getTokenByBearer(); + if (!empty(self::$current_token['uid'])) { + self::$current_user_id = self::$current_token['uid']; + } else { + self::$current_user_id = 0; + } + } if (empty(self::$current_user_id)) { @@ -220,27 +259,27 @@ class BaseApi extends BaseModule } /** - * Get the user id via the Bearer token + * Get the user token via the Bearer token * - * @return int User-ID + * @return array User Token */ - private static function getUserByBearer() + private static function getTokenByBearer() { $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (substr($authorization, 0, 7) != 'Bearer ') { - return 0; + return []; } $bearer = trim(substr($authorization, 7)); $condition = ['access_token' => $bearer]; - $token = DBA::selectFirst('application-token', ['uid'], $condition); + $token = DBA::selectFirst('application-view', ['uid', 'id', 'name', 'website', 'created_at', 'read', 'write', 'follow', 'push'], $condition); if (!DBA::isResult($token)) { Logger::warning('Token not found', $condition); - return 0; + return []; } Logger::info('Token found', $token); - return $token['uid']; + return $token; } /** @@ -307,8 +346,18 @@ class BaseApi extends BaseModule $access_token = bin2hex(random_bytes(32)); $fields = ['application-id' => $application['id'], 'uid' => $uid, 'code' => $code, 'access_token' => $access_token, 'scopes' => $scope, - 'read' => (stripos($scope, 'read') !== false), 'write' => (stripos($scope, 'write') !== false), - 'follow' => (stripos($scope, 'follow') !== false), 'created_at' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL)]; + 'read' => (stripos($scope, self::SCOPE_READ) !== false), + 'write' => (stripos($scope, self::SCOPE_WRITE) !== false), + 'follow' => (stripos($scope, self::SCOPE_FOLLOW) !== false), + 'push' => (stripos($scope, self::SCOPE_PUSH) !== false), + 'created_at' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL)]; + + foreach ([self::SCOPE_READ, self::SCOPE_WRITE, self::SCOPE_WRITE, self::SCOPE_PUSH] as $scope) { + if ($fields[$scope] && !$application[$scope]) { + Logger::warning('Requested token scope is not allowed for the application', ['token' => $fields, 'application' => $application]); + } + } + if (!DBA::insert('application-token', $fields, Database::INSERT_UPDATE)) { return []; } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index a074ef0b8..835cb3ff3 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1417); + define('DB_UPDATE_VERSION', 1418); } return [ @@ -439,6 +439,7 @@ return [ "read" => ["type" => "boolean", "comment" => "Read scope"], "write" => ["type" => "boolean", "comment" => "Write scope"], "follow" => ["type" => "boolean", "comment" => "Follow scope"], + "push" => ["type" => "boolean", "comment" => "Push scope"], ], "indexes" => [ "PRIMARY" => ["id"], @@ -457,6 +458,7 @@ return [ "read" => ["type" => "boolean", "comment" => "Read scope"], "write" => ["type" => "boolean", "comment" => "Write scope"], "follow" => ["type" => "boolean", "comment" => "Follow scope"], + "push" => ["type" => "boolean", "comment" => "Push scope"], ], "indexes" => [ "PRIMARY" => ["application-id", "uid"], diff --git a/static/dbview.config.php b/static/dbview.config.php index 324f7f926..488cbceaa 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -53,6 +53,7 @@ "read" => ["application-token", "read"], "write" => ["application-token", "write"], "follow" => ["application-token", "follow"], + "push" => ["application-token", "push"], ], "query" => "FROM `application-token` INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`" diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index f2e2a1293..2234a8c16 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2021.06-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-15 10:54+0000\n" +"POT-Creation-Date: 2021-05-16 07:41+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME