Merge pull request #10252 from annando/api-status

API: We now can post statuses via API
This commit is contained in:
Hypolite Petovan 2021-05-16 04:35:36 -04:00 committed by GitHub
commit 69824b73c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 369 additions and 79 deletions

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2021.06-dev (Siberian Iris) -- 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', `read` boolean COMMENT 'Read scope',
`write` boolean COMMENT 'Write scope', `write` boolean COMMENT 'Write scope',
`follow` boolean COMMENT 'Follow scope', `follow` boolean COMMENT 'Follow scope',
`push` boolean COMMENT 'Push scope',
PRIMARY KEY(`id`), PRIMARY KEY(`id`),
UNIQUE INDEX `client_id` (`client_id`) UNIQUE INDEX `client_id` (`client_id`)
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='OAuth application'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='OAuth application';
@ -395,6 +396,7 @@ CREATE TABLE IF NOT EXISTS `application-token` (
`read` boolean COMMENT 'Read scope', `read` boolean COMMENT 'Read scope',
`write` boolean COMMENT 'Write scope', `write` boolean COMMENT 'Write scope',
`follow` boolean COMMENT 'Follow scope', `follow` boolean COMMENT 'Follow scope',
`push` boolean COMMENT 'Push scope',
PRIMARY KEY(`application-id`,`uid`), PRIMARY KEY(`application-id`,`uid`),
INDEX `uid_id` (`uid`,`application-id`), INDEX `uid_id` (`uid`,`application-id`),
FOREIGN KEY (`application-id`) REFERENCES `application` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, 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`.`scopes` AS `scopes`,
`application-token`.`read` AS `read`, `application-token`.`read` AS `read`,
`application-token`.`write` AS `write`, `application-token`.`write` AS `write`,
`application-token`.`follow` AS `follow` `application-token`.`follow` AS `follow`,
`application-token`.`push` AS `push`
FROM `application-token` FROM `application-token`
INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`; INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`;

View file

@ -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/clear`](https://docs.joinmastodon.org/methods/notifications/)
- [`POST /api/v1/notifications/:id/dismiss`](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/) - [`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/) - [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/)
- [`DELETE /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/) - [`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) - [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity)
- [`POST /api/v1/media`](https://docs.joinmastodon.org/methods/statuses/media/) - [`POST /api/v1/media`](https://docs.joinmastodon.org/methods/statuses/media/)
- [`PUT /api/v1/media/:id`](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/) - [`GET /api/v1/timelines/direct`](https://docs.joinmastodon.org/methods/timelines/)
## Non supportable endpoints ## Non supportable endpoints

View file

@ -2115,6 +2115,32 @@ class BBCode
return array_unique($ret); 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. * Perform a custom function on a text after having escaped blocks enclosed in the provided tag list.
* *

View file

@ -54,6 +54,15 @@ class Error extends BaseFactory
System::jsonError(401, $errorobj->toArray()); 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 = '') public function InternalError(string $error = '')
{ {
$error = $error ?: DI::l10n()->t('Internal Server Error'); $error = $error ?: DI::l10n()->t('Internal Server Error');

View file

@ -35,7 +35,7 @@ class Index extends BaseApi
{ {
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
if (self::login() === false) { if (self::login(self::SCOPE_READ) === false) {
throw new HTTPException\ForbiddenException(); throw new HTTPException\ForbiddenException();
} }

View file

@ -37,7 +37,7 @@ class Show extends BaseApi
{ {
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
if (self::login() === false) { if (self::login(self::SCOPE_READ) === false) {
throw new HTTPException\ForbiddenException(); throw new HTTPException\ForbiddenException();
} }

View file

@ -33,7 +33,7 @@ class Block extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -33,7 +33,7 @@ class Follow extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {
@ -42,6 +42,6 @@ class Follow extends BaseApi
$cid = Contact::follow($parameters['id'], self::getCurrentUserID()); $cid = Contact::follow($parameters['id'], self::getCurrentUserID());
System::jsonExit(DI::mstdnRelationship()->createFromContactId($cid)->toArray()); System::jsonExit(DI::mstdnRelationship()->createFromContactId($cid, $uid)->toArray());
} }
} }

View file

@ -37,7 +37,7 @@ class Followers extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Following extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -35,7 +35,7 @@ class IdentityProofs extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
System::jsonExit([]); System::jsonExit([]);
} }

View file

@ -38,7 +38,7 @@ class Lists extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -33,7 +33,7 @@ class Mute extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -34,7 +34,7 @@ class Note extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Relationships extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($_REQUEST['id']) || !is_array($_REQUEST['id'])) { if (empty($_REQUEST['id']) || !is_array($_REQUEST['id'])) {

View file

@ -40,7 +40,7 @@ class Search extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
// What to search for // What to search for

View file

@ -33,7 +33,7 @@ class Unblock extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -33,7 +33,7 @@ class Unfollow extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -33,7 +33,7 @@ class Unmute extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -31,7 +31,7 @@ class UpdateCredentials extends BaseApi
{ {
public static function patch(array $parameters = []) public static function patch(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
$data = Network::postdata(); $data = Network::postdata();

View file

@ -38,7 +38,7 @@ class VerifyCredentials extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
$self = User::getOwnerDataById($uid); $self = User::getOwnerDataById($uid);

View file

@ -35,7 +35,7 @@ class Announcements extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
// @todo Possibly use the message from the pageheader addon for this // @todo Possibly use the message from the pageheader addon for this
System::jsonExit([]); System::jsonExit([]);

View file

@ -67,9 +67,10 @@ class Apps extends BaseApi
$fields['scopes'] = $scopes; $fields['scopes'] = $scopes;
} }
$fields['read'] = (stripos($scopes, 'read') !== false); $fields['read'] = (stripos($scopes, self::SCOPE_READ) !== false);
$fields['write'] = (stripos($scopes, 'write') !== false); $fields['write'] = (stripos($scopes, self::SCOPE_WRITE) !== false);
$fields['follow'] = (stripos($scopes, 'follow') !== false); $fields['follow'] = (stripos($scopes, self::SCOPE_FOLLOW) !== false);
$fields['push'] = (stripos($scopes, self::SCOPE_PUSH) !== false);
if (!empty($website)) { if (!empty($website)) {
$fields['website'] = $website; $fields['website'] = $website;

View file

@ -37,7 +37,7 @@ class Blocks extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -39,7 +39,7 @@ class Bookmarks extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
// Maximum number of results to return. Defaults to 20. // Maximum number of results to return. Defaults to 20.

View file

@ -40,7 +40,7 @@ class Favourited extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
// Maximum number of results to return. Defaults to 20. // Maximum number of results to return. Defaults to 20.

View file

@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, 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\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']);
}
}

View file

@ -45,7 +45,7 @@ class FollowRequests extends BaseApi
*/ */
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_FOLLOW);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
$introduction = DI::intro()->selectFirst(['id' => $parameters['id'], 'uid' => $uid]); $introduction = DI::intro()->selectFirst(['id' => $parameters['id'], 'uid' => $uid]);
@ -83,7 +83,7 @@ class FollowRequests extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
$min_id = $_GET['min_id'] ?? null; $min_id = $_GET['min_id'] ?? null;

View file

@ -33,7 +33,7 @@ class Lists extends BaseApi
{ {
public static function delete(array $parameters = []) public static function delete(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
@ -54,7 +54,7 @@ class Lists extends BaseApi
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
$title = $_REQUEST['title'] ?? ''; $title = $_REQUEST['title'] ?? '';
@ -90,7 +90,7 @@ class Lists extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -49,7 +49,7 @@ class Accounts extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -31,6 +31,8 @@ class Markers extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(self::SCOPE_WRITE);
self::unsupported('post'); self::unsupported('post');
} }
@ -40,7 +42,7 @@ class Markers extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
System::jsonExit([]); System::jsonExit([]);
} }

View file

@ -33,6 +33,9 @@ class Media extends BaseApi
{ {
public static function put(array $parameters = []) public static function put(array $parameters = [])
{ {
self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID();
$data = self::getPutData(); $data = self::getPutData();
self::unsupported('put'); self::unsupported('put');
} }
@ -43,7 +46,7 @@ class Media extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Mutes extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -39,7 +39,7 @@ class Notifications extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (!empty($parameters['id'])) { if (!empty($parameters['id'])) {

View file

@ -32,7 +32,7 @@ class Clear extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
DBA::update('notify', ['seen' => true], ['uid' => $uid]); DBA::update('notify', ['seen' => true], ['uid' => $uid]);

View file

@ -33,7 +33,7 @@ class Dismiss extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Preferences extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
$user = User::getById($uid, ['language', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']); $user = User::getById($uid, ['language', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);

View file

@ -21,12 +21,19 @@
namespace Friendica\Module\Api\Mastodon; 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\Core\System;
use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Group;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\User;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
use Friendica\Protocol\Activity;
use Friendica\Util\Images;
/** /**
* @see https://docs.joinmastodon.org/methods/statuses/ * @see https://docs.joinmastodon.org/methods/statuses/
@ -35,13 +42,155 @@ class Statuses extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID();
$data = self::getJsonPostData(); $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 = []) public static function delete(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -35,7 +35,7 @@ class Bookmark extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -35,7 +35,7 @@ class Favourite extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -34,7 +34,7 @@ class Mute extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -34,7 +34,7 @@ class Pin extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Reblog extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -35,7 +35,7 @@ class Unbookmark extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -35,7 +35,7 @@ class Unfavourite extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -34,7 +34,7 @@ class Unmute extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -34,7 +34,7 @@ class Unpin extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Unreblog extends BaseApi
{ {
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -37,7 +37,7 @@ class Suggestions extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
// Maximum number of results to return. Defaults to 40. // Maximum number of results to return. Defaults to 40.

View file

@ -39,7 +39,7 @@ class Home extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
// Return results older than id // Return results older than id

View file

@ -39,7 +39,7 @@ class ListTimeline extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['id'])) { if (empty($parameters['id'])) {

View file

@ -40,7 +40,7 @@ class Tag extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
self::login(); self::login(self::SCOPE_READ);
$uid = self::getCurrentUserID(); $uid = self::getCurrentUserID();
if (empty($parameters['hashtag'])) { if (empty($parameters['hashtag'])) {

View file

@ -39,7 +39,7 @@ abstract class ContactEndpoint extends BaseApi
{ {
parent::init($parameters); parent::init($parameters);
if (!self::login()) { if (!self::login(self::SCOPE_READ)) {
throw new HTTPException\UnauthorizedException(); throw new HTTPException\UnauthorizedException();
} }
} }

View file

@ -35,6 +35,11 @@ require_once __DIR__ . '/../../include/api.php';
class BaseApi extends BaseModule 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 * @var string json|xml|rss|atom
*/ */
@ -43,6 +48,10 @@ class BaseApi extends BaseModule
* @var bool|int * @var bool|int
*/ */
protected static $current_user_id; protected static $current_user_id;
/**
* @var array
*/
protected static $current_token = [];
public static function init(array $parameters = []) public static function init(array $parameters = [])
{ {
@ -171,6 +180,8 @@ class BaseApi extends BaseModule
* *
* Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part * Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part
* *
* @param string $scope the requested scope (read, write, follow)
*
* @return bool Was a user authenticated? * @return bool Was a user authenticated?
* @throws HTTPException\ForbiddenException * @throws HTTPException\ForbiddenException
* @throws HTTPException\UnauthorizedException * @throws HTTPException\UnauthorizedException
@ -182,10 +193,22 @@ class BaseApi extends BaseModule
* 'authenticated' => return status, * 'authenticated' => return status,
* 'user_record' => return authenticated user record * 'user_record' => return authenticated user record
*/ */
protected static function login() protected static function login(string $scope)
{ {
if (empty(self::$current_user_id)) { 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)) { if (empty(self::$current_user_id)) {
@ -198,6 +221,16 @@ class BaseApi extends BaseModule
return (bool)self::$current_user_id; 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 * Get current user id, returns 0 if not logged in
* *
@ -206,7 +239,13 @@ class BaseApi extends BaseModule
protected static function getCurrentUserID() protected static function getCurrentUserID()
{ {
if (empty(self::$current_user_id)) { 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)) { 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'] ?? ''; $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (substr($authorization, 0, 7) != 'Bearer ') { if (substr($authorization, 0, 7) != 'Bearer ') {
return 0; return [];
} }
$bearer = trim(substr($authorization, 7)); $bearer = trim(substr($authorization, 7));
$condition = ['access_token' => $bearer]; $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)) { if (!DBA::isResult($token)) {
Logger::warning('Token not found', $condition); Logger::warning('Token not found', $condition);
return 0; return [];
} }
Logger::info('Token found', $token); Logger::info('Token found', $token);
return $token['uid']; return $token;
} }
/** /**
@ -307,8 +346,18 @@ class BaseApi extends BaseModule
$access_token = bin2hex(random_bytes(32)); $access_token = bin2hex(random_bytes(32));
$fields = ['application-id' => $application['id'], 'uid' => $uid, 'code' => $code, 'access_token' => $access_token, 'scopes' => $scope, $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), 'read' => (stripos($scope, self::SCOPE_READ) !== false),
'follow' => (stripos($scope, 'follow') !== false), 'created_at' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL)]; '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)) { if (!DBA::insert('application-token', $fields, Database::INSERT_UPDATE)) {
return []; return [];
} }

View file

@ -55,7 +55,7 @@
use Friendica\Database\DBA; use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) { if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1417); define('DB_UPDATE_VERSION', 1418);
} }
return [ return [
@ -439,6 +439,7 @@ return [
"read" => ["type" => "boolean", "comment" => "Read scope"], "read" => ["type" => "boolean", "comment" => "Read scope"],
"write" => ["type" => "boolean", "comment" => "Write scope"], "write" => ["type" => "boolean", "comment" => "Write scope"],
"follow" => ["type" => "boolean", "comment" => "Follow scope"], "follow" => ["type" => "boolean", "comment" => "Follow scope"],
"push" => ["type" => "boolean", "comment" => "Push scope"],
], ],
"indexes" => [ "indexes" => [
"PRIMARY" => ["id"], "PRIMARY" => ["id"],
@ -457,6 +458,7 @@ return [
"read" => ["type" => "boolean", "comment" => "Read scope"], "read" => ["type" => "boolean", "comment" => "Read scope"],
"write" => ["type" => "boolean", "comment" => "Write scope"], "write" => ["type" => "boolean", "comment" => "Write scope"],
"follow" => ["type" => "boolean", "comment" => "Follow scope"], "follow" => ["type" => "boolean", "comment" => "Follow scope"],
"push" => ["type" => "boolean", "comment" => "Push scope"],
], ],
"indexes" => [ "indexes" => [
"PRIMARY" => ["application-id", "uid"], "PRIMARY" => ["application-id", "uid"],

View file

@ -53,6 +53,7 @@
"read" => ["application-token", "read"], "read" => ["application-token", "read"],
"write" => ["application-token", "write"], "write" => ["application-token", "write"],
"follow" => ["application-token", "follow"], "follow" => ["application-token", "follow"],
"push" => ["application-token", "push"],
], ],
"query" => "FROM `application-token` "query" => "FROM `application-token`
INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`" INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2021.06-dev\n" "Project-Id-Version: 2021.06-dev\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -833,10 +833,10 @@ msgstr ""
#: mod/unfollow.php:82 mod/wall_attach.php:78 mod/wall_attach.php:81 #: mod/unfollow.php:82 mod/wall_attach.php:78 mod/wall_attach.php:81
#: mod/wall_upload.php:99 mod/wall_upload.php:102 mod/wallmessage.php:35 #: mod/wall_upload.php:99 mod/wall_upload.php:102 mod/wallmessage.php:35
#: mod/wallmessage.php:59 mod/wallmessage.php:96 mod/wallmessage.php:120 #: mod/wallmessage.php:59 mod/wallmessage.php:96 mod/wallmessage.php:120
#: src/Module/Attach.php:56 src/Module/BaseApi.php:65 src/Module/BaseApi.php:71 #: src/Module/Attach.php:56 src/Module/BaseApi.php:74 src/Module/BaseApi.php:80
#: src/Module/BaseApi.php:78 src/Module/BaseApi.php:84 #: src/Module/BaseApi.php:87 src/Module/BaseApi.php:93
#: src/Module/BaseApi.php:91 src/Module/BaseApi.php:97 #: src/Module/BaseApi.php:100 src/Module/BaseApi.php:106
#: src/Module/BaseApi.php:104 src/Module/BaseApi.php:110 #: src/Module/BaseApi.php:113 src/Module/BaseApi.php:119
#: src/Module/BaseNotifications.php:88 src/Module/Contact.php:385 #: src/Module/BaseNotifications.php:88 src/Module/Contact.php:385
#: src/Module/Contact/Advanced.php:43 src/Module/Delegation.php:118 #: src/Module/Contact/Advanced.php:43 src/Module/Delegation.php:118
#: src/Module/FollowConfirm.php:16 src/Module/FriendSuggest.php:44 #: src/Module/FollowConfirm.php:16 src/Module/FriendSuggest.php:44
@ -4556,6 +4556,11 @@ msgid "Unauthorized"
msgstr "" msgstr ""
#: src/Factory/Api/Mastodon/Error.php:59 #: src/Factory/Api/Mastodon/Error.php:59
msgid ""
"Token is not authorized with a valid user or is missing a required scope"
msgstr ""
#: src/Factory/Api/Mastodon/Error.php:68
#: src/Module/Special/HTTPException.php:53 #: src/Module/Special/HTTPException.php:53
msgid "Internal Server Error" msgid "Internal Server Error"
msgstr "" msgstr ""
@ -7333,12 +7338,12 @@ msgstr ""
msgid "User registrations waiting for confirmation" msgid "User registrations waiting for confirmation"
msgstr "" msgstr ""
#: src/Module/BaseApi.php:124 #: src/Module/BaseApi.php:133
#, php-format #, php-format
msgid "API endpoint %s %s is not implemented" msgid "API endpoint %s %s is not implemented"
msgstr "" msgstr ""
#: src/Module/BaseApi.php:125 #: src/Module/BaseApi.php:134
msgid "" msgid ""
"The API endpoint is currently not implemented but might be in the future." "The API endpoint is currently not implemented but might be in the future."
msgstr "" msgstr ""