diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 811bb5bc5..18338ac16 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -15,13 +15,15 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en ## Implemented endpoints -- [`GET /api/v1//accounts/:id`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information) -- [`GET /api/v1//accounts/:id/statuses`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information) -- [`GET /api/v1//accounts/:id/followers`](https://docs.joinmastodon.org/methods/accounts/) -- [`GET /api/v1//accounts/:id/following`](https://docs.joinmastodon.org/methods/accounts/) +- [`GET /api/v1/accounts/:id`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information) +- [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information) +- [`GET /api/v1/accounts/:id/followers`](https://docs.joinmastodon.org/methods/accounts/) +- [`GET /api/v1/accounts/:id/following`](https://docs.joinmastodon.org/methods/accounts/) +- [`GET /api/v1/accounts/:id/lists`](https://docs.joinmastodon.org/methods/accounts/) +- [`GET /api/v1/accounts/search`](https://docs.joinmastodon.org/methods/accounts) +- [`GET /api/v1/accounts/verify_credentials`](https://docs.joinmastodon.org/methods/accounts) - [`GET /api/v1/blocks`](https://docs.joinmastodon.org/methods/accounts/blocks/) - [`GET /api/v1/bookmarks`](https://docs.joinmastodon.org/methods/accounts/bookmarks/) -- [`GET /api/v1//accounts/verify_credentials`](https://docs.joinmastodon.org/methods/accounts) - [`GET /api/v1/custom_emojis`](https://docs.joinmastodon.org/methods/instance/custom_emojis/) - Doesn't return unicode emojis since they aren't using an image URL @@ -42,7 +44,11 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance#fetch-instance) - [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains) +- [`GET /api/v1/lists`](https://docs.joinmastodon.org/methods/timelines/lists/) +- [`GET /api/v1/lists/:id`](https://docs.joinmastodon.org/methods/timelines/lists/) +- [`GET /api/v1/lists/:id/accounts`](https://docs.joinmastodon.org/methods/timelines/lists/) - [`GET /api/v1/mutes`](https://docs.joinmastodon.org/methods/accounts/mutes/) +- [`GET /api/v1/preferences`](https://docs.joinmastodon.org/methods/accounts/preferences/) - [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/statuses/:id/context`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/statuses/:id/reblogged_by`](https://docs.joinmastodon.org/methods/statuses/) diff --git a/src/DI.php b/src/DI.php index c2f53e4df..045425b88 100644 --- a/src/DI.php +++ b/src/DI.php @@ -295,6 +295,14 @@ abstract class DI return self::$dice->create(Factory\Api\Mastodon\Status::class); } + /** + * @return Factory\Api\Mastodon\ListEntity + */ + public static function mstdnList() + { + return self::$dice->create(Factory\Api\Mastodon\ListEntity::class); + } + /** * @return Factory\Api\Mastodon\Mention */ diff --git a/src/Factory/Api/Mastodon/ListEntity.php b/src/Factory/Api/Mastodon/ListEntity.php new file mode 100644 index 000000000..57547e438 --- /dev/null +++ b/src/Factory/Api/Mastodon/ListEntity.php @@ -0,0 +1,34 @@ +. + * + */ + +namespace Friendica\Factory\Api\Mastodon; + +use Friendica\BaseFactory; +use Friendica\Database\DBA; + +class ListEntity extends BaseFactory +{ + public function create(int $id) + { + $group = DBA::selectFirst('group', ['name'], ['id' => $id, 'deleted' => false]); + return new \Friendica\Object\Api\Mastodon\ListEntity($id, $group['name'] ?? '', 'list'); + } +} diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 411cf333e..876369f30 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -2763,11 +2763,12 @@ class Contact * * @param string $search Name or nick * @param string $mode Search mode (e.g. "community") + * @param int $uid User ID * * @return array with search results * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function searchByName($search, $mode = '') + public static function searchByName(string $search, string $mode = '', int $uid = 0) { if (empty($search)) { return []; @@ -2800,7 +2801,7 @@ class Contact NOT `failed` AND `uid` = ? AND (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql ORDER BY `nurl` DESC LIMIT 1000", - Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, 0, $search, $search, $search + Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, $uid, $search, $search, $search ); $contacts = DBA::toArray($results); diff --git a/src/Module/Api/Mastodon/Accounts/Followers.php b/src/Module/Api/Mastodon/Accounts/Followers.php index 739341a2f..ab2ca6839 100644 --- a/src/Module/Api/Mastodon/Accounts/Followers.php +++ b/src/Module/Api/Mastodon/Accounts/Followers.php @@ -81,6 +81,10 @@ class Followers extends BaseApi } DBA::close($followers); + if (!empty($min_id)) { + array_reverse($accounts); + } + System::jsonExit($accounts); } } diff --git a/src/Module/Api/Mastodon/Accounts/Following.php b/src/Module/Api/Mastodon/Accounts/Following.php index 10d73e907..414213dc4 100644 --- a/src/Module/Api/Mastodon/Accounts/Following.php +++ b/src/Module/Api/Mastodon/Accounts/Following.php @@ -81,6 +81,10 @@ class Following extends BaseApi } DBA::close($followers); + if (!empty($min_id)) { + array_reverse($accounts); + } + System::jsonExit($accounts); } } diff --git a/src/Module/Api/Mastodon/Accounts/Lists.php b/src/Module/Api/Mastodon/Accounts/Lists.php new file mode 100644 index 000000000..95677dad6 --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Lists.php @@ -0,0 +1,66 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Lists extends BaseApi +{ + /** + * @param array $parameters + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function rawContent(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->RecordNotFound(); + } + + $id = $parameters['id']; + if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) { + DI::mstdnError()->RecordNotFound(); + } + + $lists = []; + + $cdata = Contact::getPublicAndUserContacID($id, $uid); + if (!empty($cdata['user'])) { + $groups = DBA::select('group_member', ['gid'], ['contact-id' => $cdata['user']]); + while ($group = DBA::fetch($groups)) { + $lists[] = DI::mstdnList()->create($group['gid']); + } + DBA::close($groups); + } + + System::jsonExit($lists); + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Search.php b/src/Module/Api/Mastodon/Accounts/Search.php new file mode 100644 index 000000000..17adb5485 --- /dev/null +++ b/src/Module/Api/Mastodon/Accounts/Search.php @@ -0,0 +1,98 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Accounts; + +use Friendica\Core\Search as CoreSearch; +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Model\Contact; +use Friendica\Module\BaseApi; +use Friendica\Object\Search\ContactResult; +/** + * @see https://docs.joinmastodon.org/methods/accounts/ + */ +class Search extends BaseApi +{ + /** + * @param array $parameters + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function rawContent(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + // What to search for + $q = (int)!isset($_REQUEST['q']) ? 0 : $_REQUEST['q']; + // Maximum number of results. Defaults to 40. + $limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit']; + // Attempt WebFinger lookup. Defaults to false. Use this when q is an exact address. + $resolve = (int)!isset($_REQUEST['resolve']) ? 0 : $_REQUEST['resolve']; + // Only who the user is following. Defaults to false. + $following = (int)!isset($_REQUEST['following']) ? 0 : $_REQUEST['following']; + + $accounts = []; + + if (!$following) { + if ((strrpos($q, '@') > 0) && $resolve) { + $results = CoreSearch::getContactsFromProbe($q); + } + + if (empty($results)) { + if (DI::config()->get('system', 'poco_local_search')) { + $results = CoreSearch::getContactsFromLocalDirectory($q, CoreSearch::TYPE_ALL, 0, $limit); + } elseif (!empty(DI::config()->get('system', 'directory'))) { + $results = CoreSearch::getContactsFromGlobalDirectory($q, CoreSearch::TYPE_ALL, 1); + } + } + + if (!empty($results)) { + $counter = 0; + foreach ($results->getResults() as $result) { + if (++$counter > $limit) { + continue; + } + if ($result instanceof ContactResult) { + $id = Contact::getIdForURL($result->getUrl(), 0, false); + $accounts[] = DI::mstdnAccount()->createFromContactId($id, $uid); + } + } + } + } else { + $contacts = Contact::searchByName($q, '', $uid); + $counter = 0; + foreach ($contacts as $contact) { + if (!in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) { + continue; + } + if (++$counter > $limit) { + continue; + } + $accounts[] = DI::mstdnAccount()->createFromContactId($contact['id'], $uid); + } + DBA::close($contacts); + } + + System::jsonExit($accounts); + } +} diff --git a/src/Module/Api/Mastodon/Blocks.php b/src/Module/Api/Mastodon/Blocks.php index 7f968f64b..af8e77f88 100644 --- a/src/Module/Api/Mastodon/Blocks.php +++ b/src/Module/Api/Mastodon/Blocks.php @@ -80,6 +80,10 @@ class Blocks extends BaseApi } DBA::close($followers); + if (!empty($min_id)) { + array_reverse($accounts); + } + System::jsonExit($accounts); } } diff --git a/src/Module/Api/Mastodon/Lists.php b/src/Module/Api/Mastodon/Lists.php new file mode 100644 index 000000000..f65ef5139 --- /dev/null +++ b/src/Module/Api/Mastodon/Lists.php @@ -0,0 +1,76 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/timelines/lists/ + */ +class Lists extends BaseApi +{ + public static function delete(array $parameters = []) + { + self::unsupported('delete'); + } + + public static function post(array $parameters = []) + { + self::unsupported('post'); + } + + public static function put(array $parameters = []) + { + self::unsupported('put'); + } + + /** + * @param array $parameters + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function rawContent(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + $lists = []; + + $groups = DBA::select('group', ['id'], ['uid' => $uid, 'deleted' => false]); + while ($group = DBA::fetch($groups)) { + $lists[] = DI::mstdnList()->create($group['id']); + } + DBA::close($groups); + } else { + $id = $parameters['id']; + if (!DBA::exists('group',['uid' => $uid, 'deleted' => false])) { + DI::mstdnError()->RecordNotFound(); + } + $lists = DI::mstdnList()->create($id); + } + + System::jsonExit($lists); + } +} diff --git a/src/Module/Api/Mastodon/Lists/Accounts.php b/src/Module/Api/Mastodon/Lists/Accounts.php new file mode 100644 index 000000000..0b817cfc8 --- /dev/null +++ b/src/Module/Api/Mastodon/Lists/Accounts.php @@ -0,0 +1,102 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Lists; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/timelines/lists/ + * + * Currently the output will be unordered since we use public contact ids in the api and not user contact ids. + */ +class Accounts extends BaseApi +{ + public static function delete(array $parameters = []) + { + self::unsupported('delete'); + } + + public static function post(array $parameters = []) + { + self::unsupported('post'); + } + + /** + * @param array $parameters + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function rawContent(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + if (empty($parameters['id'])) { + DI::mstdnError()->RecordNotFound(); + } + + $id = $parameters['id']; + if (!DBA::exists('group', ['id' => $id, 'uid' => $uid])) { + DI::mstdnError()->RecordNotFound(); + } + + // Return results older than this id + $max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id']; + // Return results newer than this id + $since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id']; + // Maximum number of results to return. Defaults to 20. + $limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit']; + + + $params = ['order' => ['contact-id' => true], 'limit' => $limit]; + + $condition = ['gid' => $id]; + + if (!empty($max_id)) { + $condition = DBA::mergeConditions($condition, ["`contact-id` < ?", $max_id]); + } + + if (!empty($since_id)) { + $condition = DBA::mergeConditions($condition, ["`contact-id` > ?", $since_id]); + } + + if (!empty($min_id)) { + $condition = DBA::mergeConditions($condition, ["`contact-id` > ?", $min_id]); + + $params['order'] = ['contact-id']; + } + + $members = DBA::select('group_member', ['contact-id'], $condition, $params); + while ($member = DBA::fetch($members)) { + $accounts[] = DI::mstdnAccount()->createFromContactId($member['contact-id'], $uid); + } + DBA::close($members); + + if (!empty($min_id)) { + array_reverse($accounts); + } + + System::jsonExit($accounts); + } +} diff --git a/src/Module/Api/Mastodon/Mutes.php b/src/Module/Api/Mastodon/Mutes.php index b5ec30de4..cb981b33e 100644 --- a/src/Module/Api/Mastodon/Mutes.php +++ b/src/Module/Api/Mastodon/Mutes.php @@ -80,6 +80,10 @@ class Mutes extends BaseApi } DBA::close($followers); + if (!empty($min_id)) { + array_reverse($accounts); + } + System::jsonExit($accounts); } } diff --git a/src/Module/Api/Mastodon/Preferences.php b/src/Module/Api/Mastodon/Preferences.php new file mode 100644 index 000000000..fa2713905 --- /dev/null +++ b/src/Module/Api/Mastodon/Preferences.php @@ -0,0 +1,61 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\User; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/accounts/preferences/ + */ +class Preferences extends BaseApi +{ + /** + * @param array $parameters + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function rawContent(array $parameters = []) + { + self::login(); + $uid = self::getCurrentUserID(); + + $user = User::getById($uid, ['language', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']); + if (!empty($user['allow_cid']) || !empty($user['allow_gid']) || !empty($user['deny_cid']) || !empty($user['deny_gid'])) { + $visibility = 'private'; + } elseif (DI::pConfig()->get($uid, 'system', 'unlisted')) { + $visibility = 'unlisted'; + } else { + $visibility = 'public'; + } + + $sensitive = false; + $language = $user['language']; + $media = DI::pConfig()->get($uid, 'nsfw', 'disable') ? 'show_all' : 'default'; + $spoilers = DI::pConfig()->get($uid, 'system', 'disable_cw'); + + $preferences = new \Friendica\Object\Api\Mastodon\Preferences($visibility, $sensitive, $language, $media, $spoilers); + + System::jsonExit($preferences); + } +} diff --git a/src/Object/Api/Mastodon/ListEntity.php b/src/Object/Api/Mastodon/ListEntity.php new file mode 100644 index 000000000..b3407d28e --- /dev/null +++ b/src/Object/Api/Mastodon/ListEntity.php @@ -0,0 +1,51 @@ +. + * + */ + +namespace Friendica\Object\Api\Mastodon; + +use Friendica\BaseDataTransferObject; + +/** + * Class ListEntity + * + * @see https://docs.joinmastodon.org/entities/list/ + */ +class ListEntity extends BaseDataTransferObject +{ + /** @var string */ + protected $id; + /** @var string */ + protected $title; + + /** + * Creates an list record + * + * @param int $id + * @param string $title + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function __construct(int $id, string $title, string $policy) + { + $this->id = (string)$id; + $this->title = $title; + $this->replies_policy = $policy; + } +} diff --git a/src/Object/Api/Mastodon/Preferences.php b/src/Object/Api/Mastodon/Preferences.php new file mode 100644 index 000000000..1b644beca --- /dev/null +++ b/src/Object/Api/Mastodon/Preferences.php @@ -0,0 +1,67 @@ +. + * + */ + +namespace Friendica\Object\Api\Mastodon; + +use Friendica\App\BaseURL; +use Friendica\BaseDataTransferObject; +use Friendica\Collection\Api\Mastodon\Fields; +use Friendica\Content\Text\BBCode; +use Friendica\Database\DBA; +use Friendica\Model\Contact; +use Friendica\Util\DateTimeFormat; + +/** + * Class Preferences + * + * @see https://docs.joinmastodon.org/entities/preferences/ + */ +class Preferences extends BaseDataTransferObject +{ +// /** @var string (Enumerable, oneOf) */ +// protected $posting_default_visibility; +// /** @var bool */ +// protected $posting_default_sensitive; +// /** @var string (ISO 639-1 language two-letter code), or null*/ +// protected $posting_default_language; +// /** @var string (Enumerable, oneOf) */ +// protected $reading_expand_media; +// /** @var bool */ +// protected $reading_expand_spoilers; + + /** + * Creates a preferences record. + * + * @param BaseURL $baseUrl + * @param array $publicContact Full contact table record with uid = 0 + * @param array $apcontact Optional full apcontact table record + * @param array $userContact Optional full contact table record with uid != 0 + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function __construct(string $visibility, bool $sensitive, string $language, string $media, bool $spoilers) + { + $this->{'posting:default:visibility'} = $visibility; + $this->{'posting:default:sensitive'} = $sensitive; + $this->{'posting:default:language'} = $language; + $this->{'reading:expand:media'} = $media; + $this->{'reading:expand:spoilers'} = $spoilers; + } +} diff --git a/static/routes.config.php b/static/routes.config.php index ddc755ec1..c786ca2fd 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -62,7 +62,7 @@ return [ '/accounts/{id:\d+}/statuses' => [Module\Api\Mastodon\Accounts\Statuses::class, [R::GET ]], '/accounts/{id:\d+}/followers' => [Module\Api\Mastodon\Accounts\Followers::class, [R::GET ]], '/accounts/{id:\d+}/following' => [Module\Api\Mastodon\Accounts\Following::class, [R::GET ]], - '/accounts/{id:\d+}/lists' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo + '/accounts/{id:\d+}/lists' => [Module\Api\Mastodon\Accounts\Lists::class, [R::GET ]], '/accounts/{id:\d+}/identity_proofs' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/accounts/{id:\d+}/follow' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], '/accounts/{id:\d+}/unfollow' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], @@ -74,7 +74,7 @@ return [ '/accounts/{id:\d+}/unpin' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not implemented '/accounts/{id:\d+}/note' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], '/accounts/relationships' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo - '/accounts/search' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo + '/accounts/search' => [Module\Api\Mastodon\Accounts\Search::class, [R::GET ]], '/accounts/verify_credentials' => [Module\Api\Mastodon\Accounts\VerifyCredentials::class, [R::GET ]], '/accounts/update_credentials' => [Module\Api\Mastodon\Unimplemented::class, [R::PATCH ]], '/admin/accounts' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented @@ -108,9 +108,9 @@ return [ '/instance' => [Module\Api\Mastodon\Instance::class, [R::GET ]], '/instance/activity' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo '/instance/peers' => [Module\Api\Mastodon\Instance\Peers::class, [R::GET ]], - '/lists' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST]], // @todo - '/lists/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::PUT, R::DELETE]], // @todo - '/lists/{id:\d+}/accounts' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST, R::DELETE]], // @todo + '/lists' => [Module\Api\Mastodon\Lists::class, [R::GET, R::POST]], + '/lists/{id:\d+}' => [Module\Api\Mastodon\Lists::class, [R::GET, R::PUT, R::DELETE]], + '/lists/{id:\d+}/accounts' => [Module\Api\Mastodon\Lists\Accounts::class, [R::GET, R::POST, R::DELETE]], '/markers' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST]], // not implemented '/media' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], '/media/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::PUT]], // @todo @@ -121,7 +121,7 @@ return [ '/notifications/{id:\d+}/dismiss' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], '/polls/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/polls/{id:\d+}/votes' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not implemented - '/preferences' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo + '/preferences' => [Module\Api\Mastodon\Preferences::class, [R::GET ]], '/reports' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not implemented '/scheduled_statuses' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/scheduled_statuses/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::PUT, R::DELETE]], // not implemented