Merge pull request #10233 from annando/api-again
Some fixes for the Mastodon API
This commit is contained in:
commit
78a85df86b
37 changed files with 392 additions and 101 deletions
|
@ -255,6 +255,14 @@ abstract class DI
|
|||
return self::$dice->create(Factory\Api\Mastodon\Attachment::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Factory\Api\Mastodon\Card
|
||||
*/
|
||||
public static function mstdnCard()
|
||||
{
|
||||
return self::$dice->create(Factory\Api\Mastodon\Card::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Factory\Api\Mastodon\Emoji
|
||||
*/
|
||||
|
|
80
src/Factory/Api/Mastodon/Card.php
Normal file
80
src/Factory/Api/Mastodon/Card.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?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\Factory\Api\Mastodon;
|
||||
|
||||
use Friendica\BaseFactory;
|
||||
use Friendica\Content\Text\BBCode;
|
||||
use Friendica\Model\Post;
|
||||
use Friendica\Network\HTTPException;
|
||||
use Friendica\Util\Strings;
|
||||
|
||||
class Card extends BaseFactory
|
||||
{
|
||||
/**
|
||||
* @param int $uriId Uri-ID of the item
|
||||
* @return \Friendica\Object\Api\Mastodon\Card
|
||||
* @throws HTTPException\InternalServerErrorException
|
||||
* @throws \ImagickException
|
||||
*/
|
||||
public function createFromUriId(int $uriId)
|
||||
{
|
||||
$item = Post::selectFirst(['nody'], ['uri-id' => $uriId]);
|
||||
if (!empty($item['body'])) {
|
||||
$data = BBCode::getAttachmentData($item['body']);
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
foreach (Post\Media::getByURIId($uriId, [Post\Media::HTML]) as $attached) {
|
||||
if ((empty($data['url']) || Strings::compareLink($data['url'], $attached['url'])) &&
|
||||
(!empty($attached['description']) || !empty($attached['image']) || !empty($attached['preview']))) {
|
||||
$parts = parse_url($attached['url']);
|
||||
if (!empty($parts['scheme']) && !empty($parts['host'])) {
|
||||
if (empty($attached['publisher-name'])) {
|
||||
$attached['publisher-name'] = $parts['host'];
|
||||
}
|
||||
if (empty($attached['publisher-url']) || empty(parse_url($attached['publisher-url'], PHP_URL_SCHEME))) {
|
||||
$attached['publisher-url'] = $parts['scheme'] . '://' . $parts['host'];
|
||||
|
||||
if (!empty($parts['port'])) {
|
||||
$attached['publisher-url'] .= ':' . $parts['port'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['url'] = $attached['url'];
|
||||
$data['title'] = $attached['name'];
|
||||
$data['description'] = $attached['description'];
|
||||
$data['type'] = 'link';
|
||||
$data['author_name'] = $attached['author-name'];
|
||||
$data['author_url'] = $attached['author-url'];
|
||||
$data['provider_name'] = $attached['publisher-name'];
|
||||
$data['provider_url'] = $attached['publisher-url'];
|
||||
$data['image'] = $attached['preview'];
|
||||
$data['width'] = $attached['preview-width'];
|
||||
$data['height'] = $attached['preview-height'];
|
||||
}
|
||||
}
|
||||
|
||||
return new \Friendica\Object\Api\Mastodon\Card($data);
|
||||
}
|
||||
}
|
|
@ -35,4 +35,31 @@ class Error extends BaseFactory
|
|||
|
||||
System::jsonError(404, $errorobj->toArray());
|
||||
}
|
||||
|
||||
public function UnprocessableEntity(string $error = '')
|
||||
{
|
||||
$error = $error ?: DI::l10n()->t('Unprocessable Entity');
|
||||
$error_description = '';
|
||||
$errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
|
||||
|
||||
System::jsonError(422, $errorobj->toArray());
|
||||
}
|
||||
|
||||
public function Unauthorized(string $error = '')
|
||||
{
|
||||
$error = $error ?: DI::l10n()->t('Unauthorized');
|
||||
$error_description = '';
|
||||
$errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
|
||||
|
||||
System::jsonError(401, $errorobj->toArray());
|
||||
}
|
||||
|
||||
public function InternalError(string $error = '')
|
||||
{
|
||||
$error = $error ?: DI::l10n()->t('Internal Server Error');
|
||||
$error_description = '';
|
||||
$errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
|
||||
|
||||
System::jsonError(500, $errorobj->toArray());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,14 +86,28 @@ class Status extends BaseFactory
|
|||
|
||||
$sensitive = DBA::exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw']);
|
||||
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: ContactSelector::networkToName($item['network'], $item['author-link']));
|
||||
$mentions = DI::mstdnMention()->createFromUriId($uriId);
|
||||
$tags = DI::mstdnTag()->createFromUriId($uriId);
|
||||
|
||||
$data = BBCode::getAttachmentData($item['body']);
|
||||
$card = new \Friendica\Object\Api\Mastodon\Card($data);
|
||||
|
||||
$mentions = DI::mstdnMention()->createFromUriId($uriId);
|
||||
$tags = DI::mstdnTag()->createFromUriId($uriId);
|
||||
$card = DI::mstdnCard()->createFromUriId($uriId);
|
||||
$attachments = DI::mstdnAttachment()->createFromUriId($uriId);
|
||||
|
||||
$shared = BBCode::fetchShareAttributes($item['body']);
|
||||
if (!empty($shared['guid'])) {
|
||||
$shared_item = Post::selectFirst(['uri-id', 'plink'], ['guid' => $shared['guid']]);
|
||||
|
||||
$shared_uri_id = $shared_item['uri-id'] ?? 0;
|
||||
|
||||
$mentions = array_merge($mentions, DI::mstdnMention()->createFromUriId($shared_uri_id));
|
||||
$tags = array_merge($tags, DI::mstdnTag()->createFromUriId($shared_uri_id));
|
||||
$attachments = array_merge($attachments, DI::mstdnAttachment()->createFromUriId($shared_uri_id));
|
||||
|
||||
if (empty($card->toArray())) {
|
||||
$card = DI::mstdnCard()->createFromUriId($shared_uri_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($item['vid'] == Verb::getID(Activity::ANNOUNCE)) {
|
||||
$reshare = $this->createFromUriId($item['thr-parent-id'], $uid)->toArray();
|
||||
$reshared_item = Post::selectFirst(['title', 'body'], ['uri-id' => $item['thr-parent-id'], 'uid' => [0, $uid]]);
|
||||
|
|
|
@ -2927,7 +2927,7 @@ class Item
|
|||
DI::profiler()->saveTimestamp($stamp1, 'rendering');
|
||||
|
||||
if (isset($data['url']) && !in_array($data['url'], $ignore_links)) {
|
||||
if (!empty($data['description']) || !empty($data['image'] || !empty($data['preview']))) {
|
||||
if (!empty($data['description']) || !empty($data['image']) || !empty($data['preview'])) {
|
||||
$parts = parse_url($data['url']);
|
||||
if (!empty($parts['scheme']) && !empty($parts['host'])) {
|
||||
if (empty($data['provider_name'])) {
|
||||
|
|
|
@ -288,9 +288,13 @@ class Media
|
|||
public static function insertFromBody(int $uriid, string $body)
|
||||
{
|
||||
// Simplify image codes
|
||||
$body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
|
||||
$unshared_body = $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
|
||||
|
||||
$unshared_body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
|
||||
// Only remove the shared data from "real" reshares
|
||||
$shared = BBCode::fetchShareAttributes($body);
|
||||
if (!empty($shared['guid'])) {
|
||||
$unshared_body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
|
||||
}
|
||||
|
||||
$attachments = [];
|
||||
if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) {
|
||||
|
@ -363,8 +367,12 @@ class Media
|
|||
*/
|
||||
public static function insertFromRelevantUrl(int $uriid, string $body)
|
||||
{
|
||||
// Don't look at the shared content
|
||||
$body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
|
||||
// Only remove the shared data from "real" reshares
|
||||
$shared = BBCode::fetchShareAttributes($body);
|
||||
if (!empty($shared['guid'])) {
|
||||
// Don't look at the shared content
|
||||
$body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
|
||||
}
|
||||
|
||||
// Remove all hashtags and mentions
|
||||
$body = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '', $body);
|
||||
|
|
|
@ -38,7 +38,7 @@ class Accounts extends BaseApi
|
|||
public static function rawContent(array $parameters = [])
|
||||
{
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -41,7 +41,7 @@ class Followers extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -41,7 +41,7 @@ class Following extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -42,7 +42,7 @@ class Lists extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -43,7 +43,7 @@ class Statuses extends BaseApi
|
|||
public static function rawContent(array $parameters = [])
|
||||
{
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
@ -52,7 +52,7 @@ class Statuses extends BaseApi
|
|||
}
|
||||
|
||||
// Show only statuses with media attached? Defaults to false.
|
||||
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported
|
||||
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
|
||||
// Return results older than this id
|
||||
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
|
||||
// Return results newer than this id
|
||||
|
|
|
@ -43,12 +43,12 @@ class VerifyCredentials extends BaseApi
|
|||
|
||||
$self = User::getOwnerDataById($uid);
|
||||
if (empty($self)) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->InternalError();
|
||||
}
|
||||
|
||||
$cdata = Contact::getPublicAndUserContacID($self['id'], $uid);
|
||||
if (empty($cdata)) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->InternalError();
|
||||
}
|
||||
|
||||
// @todo Support the source property,
|
||||
|
|
|
@ -37,13 +37,13 @@ class Apps extends BaseApi
|
|||
*/
|
||||
public static function post(array $parameters = [])
|
||||
{
|
||||
$name = !isset($_REQUEST['client_name']) ? '' : $_REQUEST['client_name'];
|
||||
$redirect = !isset($_REQUEST['redirect_uris']) ? '' : $_REQUEST['redirect_uris'];
|
||||
$scopes = !isset($_REQUEST['scopes']) ? '' : $_REQUEST['scopes'];
|
||||
$website = !isset($_REQUEST['website']) ? '' : $_REQUEST['website'];
|
||||
$name = $_REQUEST['client_name'] ?? '';
|
||||
$redirect = $_REQUEST['redirect_uris'] ?? '';
|
||||
$scopes = $_REQUEST['scopes'] ?? '';
|
||||
$website = $_REQUEST['website'] ?? '';
|
||||
|
||||
if (empty($name) || empty($redirect)) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Missing parameters'));
|
||||
}
|
||||
|
||||
$client_id = bin2hex(random_bytes(32));
|
||||
|
@ -60,7 +60,7 @@ class Apps extends BaseApi
|
|||
}
|
||||
|
||||
if (!DBA::insert('application', $fields)) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->InternalError();
|
||||
}
|
||||
|
||||
System::jsonExit(DI::mstdnApplication()->createFromApplicationId(DBA::lastInsertId()));
|
||||
|
|
|
@ -41,7 +41,7 @@ class Blocks extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -44,7 +44,7 @@ class Directory extends BaseApi
|
|||
{
|
||||
$offset = (int)!isset($_REQUEST['offset']) ? 0 : $_REQUEST['offset'];
|
||||
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
|
||||
$order = !isset($_REQUEST['order']) ? 'active' : $_REQUEST['order'];
|
||||
$order = $_REQUEST['order'] ?? 'active';
|
||||
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
|
||||
|
||||
Logger::info('directory', ['offset' => $offset, 'limit' => $limit, 'order' => $order, 'local' => $local]);
|
||||
|
|
|
@ -53,7 +53,7 @@ class Accounts extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
@ -65,12 +65,18 @@ class Accounts extends BaseApi
|
|||
$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.
|
||||
// Maximum number of results. Defaults to 40. Max 40.
|
||||
// Set to 0 in order to get all accounts without pagination.
|
||||
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
|
||||
|
||||
|
||||
$params = ['order' => ['contact-id' => true], 'limit' => $limit];
|
||||
$params = ['order' => ['contact-id' => true]];
|
||||
|
||||
if ($limit != 0) {
|
||||
$params['limit'] = $limit;
|
||||
|
||||
}
|
||||
|
||||
$condition = ['gid' => $id];
|
||||
|
||||
if (!empty($max_id)) {
|
||||
|
@ -87,6 +93,8 @@ class Accounts extends BaseApi
|
|||
$params['order'] = ['contact-id'];
|
||||
}
|
||||
|
||||
$accounts = [];
|
||||
|
||||
$members = DBA::select('group_member', ['contact-id'], $condition, $params);
|
||||
while ($member = DBA::fetch($members)) {
|
||||
$accounts[] = DI::mstdnAccount()->createFromContactId($member['contact-id'], $uid);
|
||||
|
|
|
@ -46,7 +46,7 @@ class Media extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -41,7 +41,7 @@ class Mutes extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -63,7 +63,7 @@ class Notifications extends BaseApi
|
|||
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
|
||||
|
||||
// Array of types to exclude (follow, favourite, reblog, mention, poll, follow_request)
|
||||
$exclude_types = !isset($_REQUEST['exclude_types']) ? [] : $_REQUEST['exclude_types'];
|
||||
$exclude_types = $_REQUEST['exclude_types'] ?? [];
|
||||
|
||||
// Return only notifications received from this account
|
||||
$account_id = (int)!isset($_REQUEST['account_id']) ? 0 : $_REQUEST['account_id'];
|
||||
|
|
|
@ -42,7 +42,7 @@ class Statuses extends BaseApi
|
|||
public static function rawContent(array $parameters = [])
|
||||
{
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
System::jsonExit(DI::mstdnStatus()->createFromUriId($parameters['id'], self::getCurrentUserID()));
|
||||
|
|
|
@ -41,7 +41,7 @@ class Context extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
@ -54,7 +54,8 @@ class Context extends BaseApi
|
|||
$parents = [];
|
||||
$children = [];
|
||||
|
||||
$posts = Post::select(['uri-id', 'thr-parent-id'], ['parent-uri-id' => $parent['parent-uri-id']], [], false);
|
||||
$posts = Post::select(['uri-id', 'thr-parent-id'],
|
||||
['parent-uri-id' => $parent['parent-uri-id'], 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]], [], false);
|
||||
while ($post = Post::fetch($posts)) {
|
||||
if ($post['uri-id'] == $post['thr-parent-id']) {
|
||||
continue;
|
||||
|
@ -67,12 +68,24 @@ class Context extends BaseApi
|
|||
|
||||
$statuses = ['ancestors' => [], 'descendants' => []];
|
||||
|
||||
$ancestors = [];
|
||||
foreach (self::getParents($id, $parents) as $ancestor) {
|
||||
$statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
|
||||
$ancestors[$ancestor] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
|
||||
}
|
||||
|
||||
ksort($ancestors);
|
||||
foreach ($ancestors as $ancestor) {
|
||||
$statuses['ancestors'][] = $ancestor;
|
||||
}
|
||||
|
||||
$descendants = [];
|
||||
foreach (self::getChildren($id, $children) as $descendant) {
|
||||
$statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
|
||||
$descendants[] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
|
||||
}
|
||||
|
||||
ksort($descendants);
|
||||
foreach ($descendants as $descendant) {
|
||||
$statuses['descendants'][] = $descendant;
|
||||
}
|
||||
|
||||
System::jsonExit($statuses);
|
||||
|
|
|
@ -41,7 +41,7 @@ class FavouritedBy extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -41,7 +41,7 @@ class RebloggedBy extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$id = $parameters['id'];
|
||||
|
|
|
@ -43,7 +43,7 @@ class ListTimeline extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['id'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
// Return results older than id
|
||||
|
|
|
@ -46,7 +46,7 @@ class PublicTimeline extends BaseApi
|
|||
// Show only remote statuses? Defaults to false.
|
||||
$remote = (bool)!isset($_REQUEST['remote']) ? false : ($_REQUEST['remote'] == 'true');
|
||||
// Show only statuses with media attached? Defaults to false.
|
||||
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported
|
||||
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
|
||||
// Return results older than this id
|
||||
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
|
||||
// Return results newer than this id
|
||||
|
|
|
@ -44,13 +44,13 @@ class Tag extends BaseApi
|
|||
$uid = self::getCurrentUserID();
|
||||
|
||||
if (empty($parameters['hashtag'])) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
// If true, return only local statuses. Defaults to false.
|
||||
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
|
||||
// If true, return only statuses with media attachments. Defaults to false.
|
||||
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported
|
||||
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
|
||||
// Return results older than this ID.
|
||||
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
|
||||
// Return results newer than this ID.
|
||||
|
|
|
@ -110,12 +110,18 @@ class BaseApi extends BaseModule
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit execution with the message that the endpoint isn't implemented
|
||||
*
|
||||
* @param string $method
|
||||
* @return void
|
||||
*/
|
||||
public static function unsupported(string $method = 'all')
|
||||
{
|
||||
$path = DI::args()->getQueryString();
|
||||
Logger::info('Unimplemented API call', ['method' => $method, 'path' => $path, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'request' => $_REQUEST ?? []]);
|
||||
$error = DI::l10n()->t('API endpoint %s %s is not implemented', strtoupper($method), $path);
|
||||
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');;
|
||||
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');
|
||||
$errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
|
||||
System::jsonError(501, $errorobj->toArray());
|
||||
}
|
||||
|
@ -138,15 +144,14 @@ class BaseApi extends BaseModule
|
|||
*/
|
||||
protected static function login()
|
||||
{
|
||||
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
$authorization = $_SERVER['AUTHORIZATION'] ?? $authorization;
|
||||
|
||||
if (self::checkBearer($authorization)) {
|
||||
self::$current_user_id = self::getUserByBearer($authorization);
|
||||
return (bool)self::$current_user_id;
|
||||
if (empty(self::$current_user_id)) {
|
||||
self::$current_user_id = self::getUserByBearer();
|
||||
}
|
||||
|
||||
api_login(DI::app());
|
||||
if (empty(self::$current_user_id)) {
|
||||
// The execution stops here if no one is logged in
|
||||
api_login(DI::app());
|
||||
}
|
||||
|
||||
self::$current_user_id = api_user();
|
||||
|
||||
|
@ -160,15 +165,12 @@ class BaseApi extends BaseModule
|
|||
*/
|
||||
protected static function getCurrentUserID()
|
||||
{
|
||||
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
$authorization = $_SERVER['AUTHORIZATION'] ?? $authorization;
|
||||
|
||||
if (self::checkBearer($authorization)) {
|
||||
self::$current_user_id = self::getUserByBearer($authorization);
|
||||
return (int)self::$current_user_id;
|
||||
if (empty(self::$current_user_id)) {
|
||||
self::$current_user_id = self::getUserByBearer();
|
||||
}
|
||||
|
||||
if (is_null(self::$current_user_id)) {
|
||||
if (empty(self::$current_user_id)) {
|
||||
// Fetch the user id if logged in - but don't fail if not
|
||||
api_login(DI::app(), false);
|
||||
|
||||
self::$current_user_id = api_user();
|
||||
|
@ -177,14 +179,20 @@ class BaseApi extends BaseModule
|
|||
return (int)self::$current_user_id;
|
||||
}
|
||||
|
||||
private static function checkBearer(string $authorization)
|
||||
/**
|
||||
* Get the user id via the Bearer token
|
||||
*
|
||||
* @return int User-ID
|
||||
*/
|
||||
private static function getUserByBearer()
|
||||
{
|
||||
return (substr($authorization, 0, 7) == 'Bearer ');
|
||||
}
|
||||
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
|
||||
private static function getUserByBearer(string $authorization)
|
||||
{
|
||||
$bearer = trim(substr($authorization, 6));
|
||||
if (substr($authorization, 0, 7) != 'Bearer ') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$bearer = trim(substr($authorization, 7));
|
||||
$condition = ['access_token' => $bearer];
|
||||
$token = DBA::selectFirst('application-token', ['uid'], $condition);
|
||||
if (!DBA::isResult($token)) {
|
||||
|
@ -195,17 +203,30 @@ class BaseApi extends BaseModule
|
|||
return $token['uid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application record via the proved request header fields
|
||||
*
|
||||
* @return array application record
|
||||
*/
|
||||
public static function getApplication()
|
||||
{
|
||||
$redirect_uri = !isset($_REQUEST['redirect_uri']) ? '' : $_REQUEST['redirect_uri'];
|
||||
$client_id = !isset($_REQUEST['client_id']) ? '' : $_REQUEST['client_id'];
|
||||
$redirect_uri = $_REQUEST['redirect_uri'] ?? '';
|
||||
$client_id = $_REQUEST['client_id'] ?? '';
|
||||
$client_secret = $_REQUEST['client_secret'] ?? '';
|
||||
|
||||
if (empty($redirect_uri) || empty($client_id)) {
|
||||
Logger::warning('Incomplete request');
|
||||
if ((empty($redirect_uri) && empty($client_secret)) || empty($client_id)) {
|
||||
Logger::warning('Incomplete request', ['request' => $_REQUEST]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$condition = ['redirect_uri' => $redirect_uri, 'client_id' => $client_id];
|
||||
$condition = ['client_id' => $client_id];
|
||||
if (!empty($client_secret)) {
|
||||
$condition['client_secret'] = $client_secret;
|
||||
}
|
||||
if (!empty($redirect_uri)) {
|
||||
$condition['redirect_uri'] = $redirect_uri;
|
||||
}
|
||||
|
||||
$application = DBA::selectFirst('application', [], $condition);
|
||||
if (!DBA::isResult($application)) {
|
||||
Logger::warning('Application not found', $condition);
|
||||
|
@ -214,7 +235,38 @@ class BaseApi extends BaseModule
|
|||
return $application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an token for the application and user exists
|
||||
*
|
||||
* @param array $application
|
||||
* @param integer $uid
|
||||
* @return boolean
|
||||
*/
|
||||
public static function existsTokenForUser(array $application, int $uid)
|
||||
{
|
||||
return DBA::exists('application-token', ['application-id' => $application['id'], 'uid' => $uid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the token for the given application and user
|
||||
*
|
||||
* @param array $application
|
||||
* @param integer $uid
|
||||
* @return array application record
|
||||
*/
|
||||
public static function getTokenForUser(array $application, int $uid)
|
||||
{
|
||||
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and fetch an token for the application and user
|
||||
*
|
||||
* @param array $application
|
||||
* @param integer $uid
|
||||
* @return array application record
|
||||
*/
|
||||
public static function createTokenForUser(array $application, int $uid)
|
||||
{
|
||||
$code = bin2hex(random_bytes(32));
|
||||
$access_token = bin2hex(random_bytes(32));
|
||||
|
@ -226,6 +278,7 @@ class BaseApi extends BaseModule
|
|||
|
||||
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info array.
|
||||
*
|
||||
|
|
53
src/Module/OAuth/Acknowledge.php
Normal file
53
src/Module/OAuth/Acknowledge.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?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\OAuth;
|
||||
|
||||
use Friendica\Core\Renderer;
|
||||
use Friendica\DI;
|
||||
use Friendica\Module\BaseApi;
|
||||
|
||||
/**
|
||||
* Acknowledgement of OAuth requests
|
||||
*/
|
||||
class Acknowledge extends BaseApi
|
||||
{
|
||||
public static function post(array $parameters = [])
|
||||
{
|
||||
DI::session()->set('oauth_acknowledge', true);
|
||||
DI::app()->redirect(DI::session()->get('return_path'));
|
||||
}
|
||||
|
||||
public static function content(array $parameters = [])
|
||||
{
|
||||
DI::session()->set('return_path', $_REQUEST['return_path'] ?? '');
|
||||
|
||||
$o = Renderer::replaceMacros(Renderer::getMarkupTemplate('oauth_authorize.tpl'), [
|
||||
'$title' => DI::l10n()->t('Authorize application connection'),
|
||||
'$app' => ['name' => $_REQUEST['application'] ?? ''],
|
||||
'$authorize' => DI::l10n()->t('Do you want to authorize this application to access your posts and contacts, and/or create new posts for you?'),
|
||||
'$yes' => DI::l10n()->t('Yes'),
|
||||
'$no' => DI::l10n()->t('No'),
|
||||
]);
|
||||
|
||||
return $o;
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ use Friendica\DI;
|
|||
use Friendica\Module\BaseApi;
|
||||
|
||||
/**
|
||||
* Dummy class for all currently unimplemented endpoints
|
||||
* @see https://docs.joinmastodon.org/spec/oauth/
|
||||
*/
|
||||
class Authorize extends BaseApi
|
||||
{
|
||||
|
@ -36,30 +36,39 @@ class Authorize extends BaseApi
|
|||
*/
|
||||
public static function rawContent(array $parameters = [])
|
||||
{
|
||||
//return;
|
||||
|
||||
$response_type = !isset($_REQUEST['response_type']) ? '' : $_REQUEST['response_type'];
|
||||
$response_type = $_REQUEST['response_type'] ?? '';
|
||||
if ($response_type != 'code') {
|
||||
Logger::warning('Wrong or missing response type', ['response_type' => $response_type]);
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$application = self::getApplication();
|
||||
if (empty($application)) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
$request = $_REQUEST;
|
||||
unset($request['pagename']);
|
||||
$redirect = 'oauth/authorize?' . http_build_query($request);
|
||||
|
||||
$uid = local_user();
|
||||
if (empty($uid)) {
|
||||
Logger::info('Redirect to login');
|
||||
DI::app()->redirect('login?return_path=/oauth/authorize');
|
||||
DI::app()->redirect('login?return_path=' . urlencode($redirect));
|
||||
} else {
|
||||
Logger::info('Already logged in user', ['uid' => $uid]);
|
||||
}
|
||||
|
||||
$token = self::getTokenForUser($application, $uid);
|
||||
if (!self::existsTokenForUser($application, $uid) && !DI::session()->get('oauth_acknowledge')) {
|
||||
Logger::info('Redirect to acknowledge');
|
||||
DI::app()->redirect('oauth/acknowledge?' . http_build_query(['return_path' => $redirect, 'application' => $application['name']]));
|
||||
}
|
||||
|
||||
DI::session()->remove('oauth_acknowledge');
|
||||
|
||||
$token = self::createTokenForUser($application, $uid);
|
||||
if (!$token) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
DI::app()->redirect($application['redirect_uri'] . '?code=' . $token['code']);
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Friendica\Module\OAuth;
|
|||
use Friendica\Module\BaseApi;
|
||||
|
||||
/**
|
||||
* Dummy class for all currently unimplemented endpoints
|
||||
* @see https://docs.joinmastodon.org/spec/oauth/
|
||||
*/
|
||||
class Revoke extends BaseApi
|
||||
{
|
||||
|
|
|
@ -28,37 +28,37 @@ use Friendica\DI;
|
|||
use Friendica\Module\BaseApi;
|
||||
|
||||
/**
|
||||
* Dummy class for all currently unimplemented endpoints
|
||||
* @see https://docs.joinmastodon.org/spec/oauth/
|
||||
*/
|
||||
class Token extends BaseApi
|
||||
{
|
||||
public static function post(array $parameters = [])
|
||||
{
|
||||
$client_secret = !isset($_REQUEST['client_secret']) ? '' : $_REQUEST['client_secret'];
|
||||
$code = !isset($_REQUEST['code']) ? '' : $_REQUEST['code'];
|
||||
$grant_type = !isset($_REQUEST['grant_type']) ? '' : $_REQUEST['grant_type'];
|
||||
$client_secret = $_REQUEST['client_secret'] ?? '';
|
||||
$code = $_REQUEST['code'] ?? '';
|
||||
$grant_type = $_REQUEST['grant_type'] ?? '';
|
||||
|
||||
if ($grant_type != 'authorization_code') {
|
||||
Logger::warning('Wrong or missing grant type', ['grant_type' => $grant_type]);
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
Logger::warning('Unsupported or missing grant type', ['request' => $_REQUEST]);
|
||||
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Unsupported or missing grant type'));
|
||||
}
|
||||
|
||||
$application = self::getApplication();
|
||||
if (empty($application)) {
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->UnprocessableEntity();
|
||||
}
|
||||
|
||||
if ($application['client_secret'] != $client_secret) {
|
||||
Logger::warning('Wrong client secret', $client_secret);
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->Unauthorized();
|
||||
}
|
||||
|
||||
$condition = ['application-id' => $application['id'], 'code' => $code];
|
||||
|
||||
$token = DBA::selectFirst('application-token', ['access_token'], $condition);
|
||||
$token = DBA::selectFirst('application-token', ['access_token', 'created_at'], $condition);
|
||||
if (!DBA::isResult($token)) {
|
||||
Logger::warning('Token not found', $condition);
|
||||
DI::mstdnError()->RecordNotFound();
|
||||
DI::mstdnError()->Unauthorized();
|
||||
}
|
||||
|
||||
// @todo Use entity class
|
||||
|
|
|
@ -36,7 +36,7 @@ class Login extends BaseModule
|
|||
{
|
||||
public static function content(array $parameters = [])
|
||||
{
|
||||
$return_path = !isset($_REQUEST['return_path']) ? '' : $_REQUEST['return_path'];
|
||||
$return_path = $_REQUEST['return_path'] ?? '' ;
|
||||
|
||||
if (local_user()) {
|
||||
DI::baseUrl()->redirect($return_path);
|
||||
|
|
|
@ -39,9 +39,17 @@ class Card extends BaseDataTransferObject
|
|||
/** @var string */
|
||||
protected $type;
|
||||
/** @var string */
|
||||
protected $author_name;
|
||||
/** @var string */
|
||||
protected $author_url;
|
||||
/** @var string */
|
||||
protected $provider_name;
|
||||
/** @var string */
|
||||
protected $provider_url;
|
||||
/** @var int */
|
||||
protected $width;
|
||||
/** @var int */
|
||||
protected $height;
|
||||
/** @var string */
|
||||
protected $image;
|
||||
|
||||
|
@ -57,9 +65,13 @@ class Card extends BaseDataTransferObject
|
|||
$this->title = $attachment['title'] ?? '';
|
||||
$this->description = $attachment['description'] ?? '';
|
||||
$this->type = $attachment['type'] ?? '';
|
||||
$this->image = $attachment['image'] ?? '';
|
||||
$this->author_name = $attachment['author_name'] ?? '';
|
||||
$this->author_url = $attachment['author_url'] ?? '';
|
||||
$this->provider_name = $attachment['provider_name'] ?? '';
|
||||
$this->provider_url = $attachment['provider_url'] ?? '';
|
||||
$this->width = $attachment['width'] ?? 0;
|
||||
$this->height = $attachment['height'] ?? 0;
|
||||
$this->image = $attachment['image'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -134,7 +134,7 @@ class Status extends BaseDataTransferObject
|
|||
$this->mentions = $mentions;
|
||||
$this->tags = $tags;
|
||||
$this->emojis = [];
|
||||
//$this->card = $card;
|
||||
$this->card = $card->toArray() ?: null;
|
||||
$this->poll = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -331,9 +331,14 @@ return [
|
|||
'/mark/all' => [Module\Notifications\Notification::class, [R::GET]],
|
||||
'/{id:\d+}' => [Module\Notifications\Notification::class, [R::GET, R::POST]],
|
||||
],
|
||||
'/oauth/authorize' => [Module\OAuth\Authorize::class, [R::GET]],
|
||||
'/oauth/revoke' => [Module\OAuth\Revoke::class, [R::POST]],
|
||||
'/oauth/token' => [Module\OAuth\Token::class, [R::POST]],
|
||||
|
||||
'/oauth' => [
|
||||
'/acknowledge' => [Module\OAuth\Acknowledge::class, [R::GET, R::POST]],
|
||||
'/authorize' => [Module\OAuth\Authorize::class, [R::GET]],
|
||||
'/revoke' => [Module\OAuth\Revoke::class, [R::POST]],
|
||||
'/token' => [Module\OAuth\Token::class, [R::POST]],
|
||||
],
|
||||
|
||||
'/objects/{guid}[/{activity}]' => [Module\Objects::class, [R::GET]],
|
||||
|
||||
'/oembed' => [
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
|
||||
<h1>{{$title}}</h1>
|
||||
<h2>{{$title}}</h2>
|
||||
|
||||
<div class='oauthapp'>
|
||||
<img src='{{$app.icon}}'>
|
||||
<h4>{{$app.name}}</h4>
|
||||
<div class="oauthapp">
|
||||
{{if $app.icon}}<img src="{{$app.icon}}" alt="">{{/if}}
|
||||
<h3>{{$app.name}}</h3>
|
||||
</div>
|
||||
<h3>{{$authorize}}</h3>
|
||||
<p>{{$authorize}}</p>
|
||||
<form method="POST">
|
||||
<div class="settings-submit-wrapper"><input class="settings-submit" type="submit" name="oauth_yes" value="{{$yes}}" /></div>
|
||||
<div class="settings-submit-wrapper"><input class="settings-submit" type="submit" name="oauth_yes" value="{{$yes}}" /></div>
|
||||
</form>
|
||||
|
|
|
@ -3410,6 +3410,7 @@ section .profile-match-wrapper {
|
|||
.mod-home.is-not-singleuser nav.navbar,
|
||||
.mod-login nav.navbar {
|
||||
background-color: transparent;
|
||||
position: inherit;
|
||||
}
|
||||
.mod-home.is-not-singleuser #topbar-second,
|
||||
.mod-login #topbar-second {
|
||||
|
|
Loading…
Reference in a new issue