friendica/include/api.php

6179 lines
177 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @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/>.
*
* Friendica implementation of statusnet/twitter API
*
* @file include/api.php
* @todo Automatically detect if incoming data is HTML or BBCode
*/
use Friendica\App;
use Friendica\Content\ContactSelector;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Session;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\Mail;
use Friendica\Model\Notify;
use Friendica\Model\Photo;
use Friendica\Model\User;
use Friendica\Model\UserItem;
use Friendica\Network\FKOAuth1;
use Friendica\Network\HTTPException;
use Friendica\Network\HTTPException\BadRequestException;
use Friendica\Network\HTTPException\ExpectationFailedException;
use Friendica\Network\HTTPException\ForbiddenException;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\HTTPException\MethodNotAllowedException;
use Friendica\Network\HTTPException\NotFoundException;
use Friendica\Network\HTTPException\NotImplementedException;
use Friendica\Network\HTTPException\TooManyRequestsException;
use Friendica\Network\HTTPException\UnauthorizedException;
use Friendica\Object\Image;
use Friendica\Protocol\Activity;
use Friendica\Protocol\Diaspora;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images;
use Friendica\Util\Network;
use Friendica\Util\Proxy as ProxyUtils;
use Friendica\Util\Strings;
use Friendica\Util\XML;
require_once __DIR__ . '/../mod/share.php';
require_once __DIR__ . '/../mod/item.php';
require_once __DIR__ . '/../mod/wall_upload.php';
define('API_METHOD_ANY', '*');
define('API_METHOD_GET', 'GET');
define('API_METHOD_POST', 'POST,PUT');
define('API_METHOD_DELETE', 'POST,DELETE');
define('API_LOG_PREFIX', 'API {action} - ');
$API = [];
$called_api = [];
/**
* Auth API user
*
* It is not sufficient to use local_user() to check whether someone is allowed to use the API,
* because this will open CSRF holes (just embed an image with src=friendicasite.com/api/statuses/update?status=CSRF
* into a page, and visitors will post something without noticing it).
*/
function api_user()
{
if (!empty($_SESSION['allow_api'])) {
return local_user();
}
return false;
}
/**
* Get source name from API client
*
* Clients can send 'source' parameter to be show in post metadata
* as "sent via <source>".
* Some clients doesn't send a source param, we support ones we know
* (only Twidere, atm)
*
* @return string
* Client source name, default to "api" if unset/unknown
* @throws Exception
*/
function api_source()
{
if (requestdata('source')) {
return requestdata('source');
}
// Support for known clients that doesn't send a source name
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
if(strpos($_SERVER['HTTP_USER_AGENT'], "Twidere") !== false) {
return "Twidere";
}
Logger::info(API_LOG_PREFIX . 'Unrecognized user-agent', ['module' => 'api', 'action' => 'source', 'http_user_agent' => $_SERVER['HTTP_USER_AGENT']]);
} else {
Logger::info(API_LOG_PREFIX . 'Empty user-agent', ['module' => 'api', 'action' => 'source']);
}
return "api";
}
/**
* Format date for API
*
* @param string $str Source date, as UTC
* @return string Date in UTC formatted as "D M d H:i:s +0000 Y"
* @throws Exception
*/
function api_date($str)
{
// Wed May 23 06:01:13 +0000 2007
return DateTimeFormat::utc($str, "D M d H:i:s +0000 Y");
}
/**
* Register a function to be the endpoint for defined API path.
*
* @param string $path API URL path, relative to DI::baseUrl()
* @param string $func Function name to call on path request
* @param bool $auth API need logged user
* @param string $method HTTP method reqiured to call this endpoint.
* One of API_METHOD_ANY, API_METHOD_GET, API_METHOD_POST.
* Default to API_METHOD_ANY
*/
function api_register_func($path, $func, $auth = false, $method = API_METHOD_ANY)
{
global $API;
$API[$path] = [
'func' => $func,
'auth' => $auth,
'method' => $method,
];
// Workaround for hotot
$path = str_replace("api/", "api/1.1/", $path);
$API[$path] = [
'func' => $func,
'auth' => $auth,
'method' => $method,
];
}
/**
* Log in user via OAuth1 or Simple HTTP Auth.
* Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part
*
* @param App $a App
* @throws ForbiddenException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @hook 'authenticate'
* array $addon_auth
* 'username' => username from login form
* 'password' => password from login form
* 'authenticated' => return status,
* 'user_record' => return authenticated user record
*/
function api_login(App $a)
{
$oauth1 = new FKOAuth1();
// login with oauth
try {
$request = OAuthRequest::from_request();
list($consumer, $token) = $oauth1->verify_request($request);
if (!is_null($token)) {
$oauth1->loginUser($token->uid);
Session::set('allow_api', true);
return;
}
echo __FILE__.__LINE__.__FUNCTION__ . "<pre>";
var_dump($consumer, $token);
die();
} catch (Exception $e) {
Logger::warning(API_LOG_PREFIX . 'error', ['module' => 'api', 'action' => 'login', 'exception' => $e->getMessage()]);
}
// workaround for HTTP-auth in CGI mode
if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
$userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
if (strlen($userpass)) {
list($name, $password) = explode(':', $userpass);
$_SERVER['PHP_AUTH_USER'] = $name;
$_SERVER['PHP_AUTH_PW'] = $password;
}
}
if (empty($_SERVER['PHP_AUTH_USER'])) {
Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]);
header('WWW-Authenticate: Basic realm="Friendica"');
throw new UnauthorizedException("This API requires login");
}
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
$password = $_SERVER['PHP_AUTH_PW'] ?? '';
// allow "user@server" login (but ignore 'server' part)
$at = strstr($user, "@", true);
if ($at) {
$user = $at;
}
// next code from mod/auth.php. needs better solution
$record = null;
$addon_auth = [
'username' => trim($user),
'password' => trim($password),
'authenticated' => 0,
'user_record' => null,
];
/*
* An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record
* Addons should never set 'authenticated' except to indicate success - as hooks may be chained
* and later addons should not interfere with an earlier one that succeeded.
*/
Hook::callAll('authenticate', $addon_auth);
if ($addon_auth['authenticated'] && count($addon_auth['user_record'])) {
$record = $addon_auth['user_record'];
} else {
$user_id = User::authenticate(trim($user), trim($password), true);
if ($user_id !== false) {
$record = DBA::selectFirst('user', [], ['uid' => $user_id]);
}
}
if (!DBA::isResult($record)) {
Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]);
header('WWW-Authenticate: Basic realm="Friendica"');
//header('HTTP/1.0 401 Unauthorized');
//die('This api requires login');
throw new UnauthorizedException("This API requires login");
}
DI::auth()->setForUser($a, $record);
$_SESSION["allow_api"] = true;
Hook::callAll('logged_in', $a->user);
}
/**
* Check HTTP method of called API
*
* API endpoints can define which HTTP method to accept when called.
* This function check the current HTTP method agains endpoint
* registered method.
*
* @param string $method Required methods, uppercase, separated by comma
* @return bool
*/
function api_check_method($method)
{
if ($method == "*") {
return true;
}
return (stripos($method, $_SERVER['REQUEST_METHOD'] ?? 'GET') !== false);
}
/**
* Main API entry point
*
* Authenticate user, call registered API function, set HTTP headers
*
* @param App $a App
* @param App\Arguments $args The app arguments (optional, will retrieved by the DI-Container in case of missing)
* @return string|array API call result
* @throws Exception
*/
function api_call(App $a, App\Arguments $args = null)
{
global $API, $called_api;
if ($args == null) {
$args = DI::args();
}
$type = "json";
if (strpos($args->getQueryString(), ".xml") > 0) {
$type = "xml";
}
if (strpos($args->getQueryString(), ".json") > 0) {
$type = "json";
}
if (strpos($args->getQueryString(), ".rss") > 0) {
$type = "rss";
}
if (strpos($args->getQueryString(), ".atom") > 0) {
$type = "atom";
}
try {
foreach ($API as $p => $info) {
if (strpos($args->getQueryString(), $p) === 0) {
if (!api_check_method($info['method'])) {
throw new MethodNotAllowedException();
}
$called_api = explode("/", $p);
if (!empty($info['auth']) && api_user() === false) {
api_login($a);
}
Logger::info(API_LOG_PREFIX . 'username {username}', ['module' => 'api', 'action' => 'call', 'username' => $a->user['username']]);
Logger::debug(API_LOG_PREFIX . 'parameters', ['module' => 'api', 'action' => 'call', 'parameters' => $_REQUEST]);
$stamp = microtime(true);
$return = call_user_func($info['func'], $type);
$duration = floatval(microtime(true) - $stamp);
Logger::info(API_LOG_PREFIX . 'username {username}', ['module' => 'api', 'action' => 'call', 'username' => $a->user['username'], 'duration' => round($duration, 2)]);
DI::profiler()->saveLog(DI::logger(), API_LOG_PREFIX . 'performance');
if (false === $return) {
/*
* api function returned false withour throw an
* exception. This should not happend, throw a 500
*/
throw new InternalServerErrorException();
}
switch ($type) {
case "xml":
header("Content-Type: text/xml");
break;
case "json":
header("Content-Type: application/json");
if (!empty($return)) {
$json = json_encode(end($return));
if (!empty($_GET['callback'])) {
$json = $_GET['callback'] . "(" . $json . ")";
}
$return = $json;
}
break;
case "rss":
header("Content-Type: application/rss+xml");
$return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
break;
case "atom":
header("Content-Type: application/atom+xml");
$return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
break;
}
return $return;
}
}
Logger::warning(API_LOG_PREFIX . 'not implemented', ['module' => 'api', 'action' => 'call', 'query' => DI::args()->getQueryString()]);
throw new NotImplementedException();
} catch (HTTPException $e) {
header("HTTP/1.1 {$e->getCode()} {$e->httpdesc}");
return api_error($type, $e, $args);
}
}
/**
* Format API error string
*
* @param string $type Return type (xml, json, rss, as)
* @param object $e HTTPException Error object
* @param App\Arguments $args The App arguments
* @return string|array error message formatted as $type
*/
function api_error($type, $e, App\Arguments $args)
{
$error = ($e->getMessage() !== "" ? $e->getMessage() : $e->httpdesc);
/// @TODO: https://dev.twitter.com/overview/api/response-codes
$error = ["error" => $error,
"code" => $e->getCode() . " " . $e->httpdesc,
"request" => $args->getQueryString()];
$return = api_format_data('status', $type, ['status' => $error]);
switch ($type) {
case "xml":
header("Content-Type: text/xml");
break;
case "json":
header("Content-Type: application/json");
$return = json_encode($return);
break;
case "rss":
header("Content-Type: application/rss+xml");
break;
case "atom":
header("Content-Type: application/atom+xml");
break;
}
return $return;
}
/**
* Set values for RSS template
*
* @param App $a
* @param array $arr Array to be passed to template
* @param array $user_info User info
* @return array
* @throws BadRequestException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @todo find proper type-hints
*/
function api_rss_extra(App $a, $arr, $user_info)
{
if (is_null($user_info)) {
$user_info = api_get_user($a);
}
$arr['$user'] = $user_info;
$arr['$rss'] = [
'alternate' => $user_info['url'],
'self' => DI::baseUrl() . "/" . DI::args()->getQueryString(),
'base' => DI::baseUrl(),
'updated' => api_date(null),
'atom_updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'language' => $user_info['lang'],
'logo' => DI::baseUrl() . "/images/friendica-32.png",
];
return $arr;
}
/**
* Unique contact to contact url.
*
* @param int $id Contact id
* @return bool|string
* Contact url or False if contact id is unknown
* @throws Exception
*/
function api_unique_id_to_nurl($id)
{
$r = DBA::selectFirst('contact', ['nurl'], ['id' => $id]);
if (DBA::isResult($r)) {
return $r["nurl"];
} else {
return false;
}
}
/**
* Get user info array.
*
* @param App $a App
* @param int|string $contact_id Contact ID or URL
* @return array|bool
* @throws BadRequestException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_get_user(App $a, $contact_id = null)
{
global $called_api;
$user = null;
$extra_query = "";
$url = "";
Logger::info(API_LOG_PREFIX . 'Fetching data for user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $contact_id]);
// Searching for contact URL
if (!is_null($contact_id) && (intval($contact_id) == 0)) {
$user = DBA::escape(Strings::normaliseLink($contact_id));
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
}
// Searching for contact id with uid = 0
if (!is_null($contact_id) && (intval($contact_id) != 0)) {
$user = DBA::escape(api_unique_id_to_nurl(intval($contact_id)));
if ($user == "") {
throw new BadRequestException("User ID ".$contact_id." not found.");
}
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
}
if (is_null($user) && !empty($_GET['user_id'])) {
$user = DBA::escape(api_unique_id_to_nurl($_GET['user_id']));
if ($user == "") {
throw new BadRequestException("User ID ".$_GET['user_id']." not found.");
}
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
}
if (is_null($user) && !empty($_GET['screen_name'])) {
$user = DBA::escape($_GET['screen_name']);
$extra_query = "AND `contact`.`nick` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=".intval(api_user());
}
}
if (is_null($user) && !empty($_GET['profileurl'])) {
$user = DBA::escape(Strings::normaliseLink($_GET['profileurl']));
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=".intval(api_user());
}
}
// $called_api is the API path exploded on / and is expected to have at least 2 elements
if (is_null($user) && ($a->argc > (count($called_api) - 1)) && (count($called_api) > 0)) {
$argid = count($called_api);
if (!empty($a->argv[$argid])) {
$data = explode(".", $a->argv[$argid]);
if (count($data) > 1) {
list($user, $null) = $data;
}
}
if (is_numeric($user)) {
$user = DBA::escape(api_unique_id_to_nurl(intval($user)));
if ($user != "") {
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
}
} else {
$user = DBA::escape($user);
$extra_query = "AND `contact`.`nick` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
}
}
Logger::info(API_LOG_PREFIX . 'getting user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $user]);
if (!$user) {
if (api_user() === false) {
api_login($a);
return false;
} else {
$user = $_SESSION['uid'];
$extra_query = "AND `contact`.`uid` = %d AND `contact`.`self` ";
}
}
Logger::info(API_LOG_PREFIX . 'found user {user}', ['module' => 'api', 'action' => 'get_user', 'user' => $user, 'extra_query' => $extra_query]);
// user info
$uinfo = q(
"SELECT *, `contact`.`id` AS `cid` FROM `contact`
WHERE 1
$extra_query",
$user
);
// Selecting the id by priority, friendica first
if (is_array($uinfo)) {
api_best_nickname($uinfo);
}
// if the contact wasn't found, fetch it from the contacts with uid = 0
if (!DBA::isResult($uinfo)) {
if ($url == "") {
throw new BadRequestException("User not found.");
}
$contact = DBA::selectFirst('contact', [], ['uid' => 0, 'nurl' => Strings::normaliseLink($url)]);
if (DBA::isResult($contact)) {
$ret = [
'id' => $contact["id"],
'id_str' => (string) $contact["id"],
'name' => $contact["name"],
'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']),
'location' => ($contact["location"] != "") ? $contact["location"] : ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol']),
'description' => BBCode::toPlaintext($contact["about"]),
'profile_image_url' => $contact["micro"],
'profile_image_url_https' => $contact["micro"],
'profile_image_url_profile_size' => $contact["thumb"],
'profile_image_url_large' => $contact["photo"],
'url' => $contact["url"],
'protected' => false,
'followers_count' => 0,
'friends_count' => 0,
'listed_count' => 0,
'created_at' => api_date($contact["created"]),
'favourites_count' => 0,
'utc_offset' => 0,
'time_zone' => 'UTC',
'geo_enabled' => false,
'verified' => false,
'statuses_count' => 0,
'lang' => '',
'contributors_enabled' => false,
'is_translator' => false,
'is_translation_enabled' => false,
'following' => false,
'follow_request_sent' => false,
'statusnet_blocking' => false,
'notifications' => false,
'statusnet_profile_url' => $contact["url"],
'uid' => 0,
'cid' => Contact::getIdForURL($contact["url"], api_user(), true),
'pid' => Contact::getIdForURL($contact["url"], 0, true),
'self' => 0,
'network' => $contact["network"],
];
return $ret;
} else {
throw new BadRequestException("User ".$url." not found.");
}
}
if ($uinfo[0]['self']) {
if ($uinfo[0]['network'] == "") {
$uinfo[0]['network'] = Protocol::DFRN;
}
$usr = DBA::selectFirst('user', ['default-location'], ['uid' => api_user()]);
$profile = DBA::selectFirst('profile', ['about'], ['uid' => api_user(), 'is-default' => true]);
}
$countitems = 0;
$countfriends = 0;
$countfollowers = 0;
$starred = 0;
$pcontact_id = Contact::getIdForURL($uinfo[0]['url'], 0, true);
if (!empty($profile['about'])) {
$description = $profile['about'];
} else {
$description = $uinfo[0]["about"];
}
if (!empty($usr['default-location'])) {
$location = $usr['default-location'];
} elseif (!empty($uinfo[0]["location"])) {
$location = $uinfo[0]["location"];
} else {
$location = ContactSelector::networkToName($uinfo[0]['network'], $uinfo[0]['url'], $uinfo[0]['protocol']);
}
$ret = [
'id' => intval($pcontact_id),
'id_str' => (string) intval($pcontact_id),
'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']),
'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']),
'location' => $location,
'description' => BBCode::toPlaintext($description),
'profile_image_url' => $uinfo[0]['micro'],
'profile_image_url_https' => $uinfo[0]['micro'],
'profile_image_url_profile_size' => $uinfo[0]["thumb"],
'profile_image_url_large' => $uinfo[0]["photo"],
'url' => $uinfo[0]['url'],
'protected' => false,
'followers_count' => intval($countfollowers),
'friends_count' => intval($countfriends),
'listed_count' => 0,
'created_at' => api_date($uinfo[0]['created']),
'favourites_count' => intval($starred),
'utc_offset' => "0",
'time_zone' => 'UTC',
'geo_enabled' => false,
'verified' => true,
'statuses_count' => intval($countitems),
'lang' => '',
'contributors_enabled' => false,
'is_translator' => false,
'is_translation_enabled' => false,
'following' => (($uinfo[0]['rel'] == Contact::FOLLOWER) || ($uinfo[0]['rel'] == Contact::FRIEND)),
'follow_request_sent' => false,
'statusnet_blocking' => false,
'notifications' => false,
/// @TODO old way?
//'statusnet_profile_url' => DI::baseUrl()."/contact/".$uinfo[0]['cid'],
'statusnet_profile_url' => $uinfo[0]['url'],
'uid' => intval($uinfo[0]['uid']),
'cid' => intval($uinfo[0]['cid']),
'pid' => Contact::getIdForURL($uinfo[0]["url"], 0, true),
'self' => $uinfo[0]['self'],
'network' => $uinfo[0]['network'],
];
// If this is a local user and it uses Frio, we can get its color preferences.
if ($ret['self']) {
$theme_info = DBA::selectFirst('user', ['theme'], ['uid' => $ret['uid']]);
if ($theme_info['theme'] === 'frio') {
$schema = DI::pConfig()->get($ret['uid'], 'frio', 'schema');
if ($schema && ($schema != '---')) {
if (file_exists('view/theme/frio/schema/'.$schema.'.php')) {
$schemefile = 'view/theme/frio/schema/'.$schema.'.php';
require_once $schemefile;
}
} else {
$nav_bg = DI::pConfig()->get($ret['uid'], 'frio', 'nav_bg');
$link_color = DI::pConfig()->get($ret['uid'], 'frio', 'link_color');
$bgcolor = DI::pConfig()->get($ret['uid'], 'frio', 'background_color');
}
if (empty($nav_bg)) {
$nav_bg = "#708fa0";
}
if (empty($link_color)) {
$link_color = "#6fdbe8";
}
if (empty($bgcolor)) {
$bgcolor = "#ededed";
}
$ret['profile_sidebar_fill_color'] = str_replace('#', '', $nav_bg);
$ret['profile_link_color'] = str_replace('#', '', $link_color);
$ret['profile_background_color'] = str_replace('#', '', $bgcolor);
}
}
return $ret;
}
/**
* return api-formatted array for item's author and owner
*
* @param App $a App
* @param array $item item from db
* @return array(array:author, array:owner)
* @throws BadRequestException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_item_get_user(App $a, $item)
{
$status_user = api_get_user($a, $item['author-id'] ?? null);
$author_user = $status_user;
$status_user["protected"] = isset($item['private']) && ($item['private'] == Item::PRIVATE);
if (($item['thr-parent'] ?? '') == ($item['uri'] ?? '')) {
$owner_user = api_get_user($a, $item['owner-id'] ?? null);
} else {
$owner_user = $author_user;
}
return ([$status_user, $author_user, $owner_user]);
}
/**
* walks recursively through an array with the possibility to change value and key
*
* @param array $array The array to walk through
* @param callable $callback The callback function
*
* @return array the transformed array
*/
function api_walk_recursive(array &$array, callable $callback)
{
$new_array = [];
foreach ($array as $k => $v) {
if (is_array($v)) {
if ($callback($v, $k)) {
$new_array[$k] = api_walk_recursive($v, $callback);
}
} else {
if ($callback($v, $k)) {
$new_array[$k] = $v;
}
}
}
$array = $new_array;
return $array;
}
/**
* Callback function to transform the array in an array that can be transformed in a XML file
*
* @param mixed $item Array item value
* @param string $key Array key
*
* @return boolean Should the array item be deleted?
*/
function api_reformat_xml(&$item, &$key)
{
if (is_bool($item)) {
$item = ($item ? "true" : "false");
}
if (substr($key, 0, 10) == "statusnet_") {
$key = "statusnet:".substr($key, 10);
} elseif (substr($key, 0, 10) == "friendica_") {
$key = "friendica:".substr($key, 10);
}
/// @TODO old-lost code?
//else
// $key = "default:".$key;
return true;
}
/**
* Creates the XML from a JSON style array
*
* @param array $data JSON style array
* @param string $root_element Name of the root element
*
* @return string The XML data
*/
function api_create_xml(array $data, $root_element)
{
$childname = key($data);
$data2 = array_pop($data);
$namespaces = ["" => "http://api.twitter.com",
"statusnet" => "http://status.net/schema/api/1/",
"friendica" => "http://friendi.ca/schema/api/1/",
"georss" => "http://www.georss.org/georss"];
/// @todo Auto detection of needed namespaces
if (in_array($root_element, ["ok", "hash", "config", "version", "ids", "notes", "photos"])) {
$namespaces = [];
}
if (is_array($data2)) {
$key = key($data2);
api_walk_recursive($data2, "api_reformat_xml");
if ($key == "0") {
$data4 = [];
$i = 1;
foreach ($data2 as $item) {
$data4[$i++ . ":" . $childname] = $item;
}
$data2 = $data4;
}
}
$data3 = [$root_element => $data2];
$ret = XML::fromArray($data3, $xml, false, $namespaces);
return $ret;
}
/**
* Formats the data according to the data type
*
* @param string $root_element Name of the root element
* @param string $type Return type (atom, rss, xml, json)
* @param array $data JSON style array
*
* @return array|string (string|array) XML data or JSON data
*/
function api_format_data($root_element, $type, $data)
{
switch ($type) {
case "atom":
case "rss":
case "xml":
$ret = api_create_xml($data, $root_element);
break;
case "json":
default:
$ret = $data;
break;
}
return $ret;
}
/**
* TWITTER API
*/
/**
* Returns an HTTP 200 OK response code and a representation of the requesting user if authentication was successful;
* returns a 401 status code and an error message if not.
*
* @see https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
*
* @param string $type Return type (atom, rss, xml, json)
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_account_verify_credentials($type)
{
$a = DI::app();
if (api_user() === false) {
throw new ForbiddenException();
}
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
unset($_REQUEST["screen_name"]);
unset($_GET["screen_name"]);
$skip_status = $_REQUEST['skip_status'] ?? false;
$user_info = api_get_user($a);
// "verified" isn't used here in the standard
unset($user_info["verified"]);
// - Adding last status
if (!$skip_status) {
$item = api_get_last_status($user_info['pid'], $user_info['uid']);
if (!empty($item)) {
$user_info['status'] = api_format_item($item, $type);
}
}
// "uid" and "self" are only needed for some internal stuff, so remove it from here
unset($user_info["uid"]);
unset($user_info["self"]);
return api_format_data("user", $type, ['user' => $user_info]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/account/verify_credentials', 'api_account_verify_credentials', true);
/**
* Get data from $_POST or $_GET
*
* @param string $k
* @return null
*/
function requestdata($k)
{
if (!empty($_POST[$k])) {
return $_POST[$k];
}
if (!empty($_GET[$k])) {
return $_GET[$k];
}
return null;
}
/**
* Deprecated function to upload media.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_statuses_mediap($type)
{
$a = DI::app();
if (api_user() === false) {
Logger::log('api_statuses_update: no user');
throw new ForbiddenException();
}
$user_info = api_get_user($a);
$_REQUEST['profile_uid'] = api_user();
$_REQUEST['api_source'] = true;
$txt = requestdata('status');
/// @TODO old-lost code?
//$txt = urldecode(requestdata('status'));
if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
$txt = HTML::toBBCodeVideo($txt);
$config = HTMLPurifier_Config::createDefault();
$config->set('Cache.DefinitionImpl', null);
$purifier = new HTMLPurifier($config);
$txt = $purifier->purify($txt);
}
$txt = HTML::toBBCode($txt);
$a->argv[1] = $user_info['screen_name']; //should be set to username?
$picture = wall_upload_post($a, false);
// now that we have the img url in bbcode we can add it to the status and insert the wall item.
$_REQUEST['body'] = $txt . "\n\n" . '[url=' . $picture["albumpage"] . '][img]' . $picture["preview"] . "[/img][/url]";
$item_id = item_post($a);
// output the post that we just posted.
return api_status_show($type, $item_id);
}
/// @TODO move this to top of file or somewhere better!
api_register_func('api/statuses/mediap', 'api_statuses_mediap', true, API_METHOD_POST);
/**
* Updates the user’s current status.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws TooManyRequestsException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
*/
function api_statuses_update($type)
{
$a = DI::app();
if (api_user() === false) {
Logger::log('api_statuses_update: no user');
throw new ForbiddenException();
}
api_get_user($a);
// convert $_POST array items to the form we use for web posts.
if (requestdata('htmlstatus')) {
$txt = requestdata('htmlstatus');
if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
$txt = HTML::toBBCodeVideo($txt);
$config = HTMLPurifier_Config::createDefault();
$config->set('Cache.DefinitionImpl', null);
$purifier = new HTMLPurifier($config);
$txt = $purifier->purify($txt);
$_REQUEST['body'] = HTML::toBBCode($txt);
}
} else {
$_REQUEST['body'] = requestdata('status');
}
$_REQUEST['title'] = requestdata('title');
$parent = requestdata('in_reply_to_status_id');
// Twidere sends "-1" if it is no reply ...
if ($parent == -1) {
$parent = "";
}
if (ctype_digit($parent)) {
$_REQUEST['parent'] = $parent;
} else {
$_REQUEST['parent_uri'] = $parent;
}
if (requestdata('lat') && requestdata('long')) {
$_REQUEST['coord'] = sprintf("%s %s", requestdata('lat'), requestdata('long'));
}
$_REQUEST['profile_uid'] = api_user();
if (!$parent) {
// Check for throttling (maximum posts per day, week and month)
$throttle_day = DI::config()->get('system', 'throttle_limit_day');
if ($throttle_day > 0) {
$datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60);
$condition = ["`uid` = ? AND `wall` AND `received` > ?", api_user(), $datefrom];
$posts_day = DBA::count('thread', $condition);
if ($posts_day > $throttle_day) {
Logger::log('Daily posting limit reached for user '.api_user(), Logger::DEBUG);
// die(api_error($type, DI::l10n()->t("Daily posting limit of %d posts reached. The post was rejected.", $throttle_day));
throw new TooManyRequestsException(DI::l10n()->tt("Daily posting limit of %d post reached. The post was rejected.", "Daily posting limit of %d posts reached. The post was rejected.", $throttle_day));
}
}
$throttle_week = DI::config()->get('system', 'throttle_limit_week');
if ($throttle_week > 0) {
$datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*7);
$condition = ["`uid` = ? AND `wall` AND `received` > ?", api_user(), $datefrom];
$posts_week = DBA::count('thread', $condition);
if ($posts_week > $throttle_week) {
Logger::log('Weekly posting limit reached for user '.api_user(), Logger::DEBUG);
// die(api_error($type, DI::l10n()->t("Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week)));
throw new TooManyRequestsException(DI::l10n()->tt("Weekly posting limit of %d post reached. The post was rejected.", "Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week));
}
}
$throttle_month = DI::config()->get('system', 'throttle_limit_month');
if ($throttle_month > 0) {
$datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*30);
$condition = ["`uid` = ? AND `wall` AND `received` > ?", api_user(), $datefrom];
$posts_month = DBA::count('thread', $condition);
if ($posts_month > $throttle_month) {
Logger::log('Monthly posting limit reached for user '.api_user(), Logger::DEBUG);
// die(api_error($type, DI::l10n()->t("Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month));
throw new TooManyRequestsException(DI::l10n()->t("Monthly posting limit of %d post reached. The post was rejected.", "Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month));
}
}
}
if (!empty($_FILES['media'])) {
// upload the image if we have one
$picture = wall_upload_post($a, false);
if (is_array($picture)) {
$_REQUEST['body'] .= "\n\n" . '[url=' . $picture["albumpage"] . '][img]' . $picture["preview"] . "[/img][/url]";
}
}
if (requestdata('media_ids')) {
$ids = explode(',', requestdata('media_ids'));
foreach ($ids as $id) {
$r = q(
"SELECT `resource-id`, `scale`, `nickname`, `type`, `desc` FROM `photo` INNER JOIN `user` ON `user`.`uid` = `photo`.`uid` WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = %d) AND `scale` > 0 AND `photo`.`uid` = %d ORDER BY `photo`.`width` DESC LIMIT 1",
intval($id),
api_user()
);
if (DBA::isResult($r)) {
$phototypes = Images::supportedTypes();
$ext = $phototypes[$r[0]['type']];
$description = $r[0]['desc'] ?? '';
$_REQUEST['body'] .= "\n\n" . '[url=' . DI::baseUrl() . '/photos/' . $r[0]['nickname'] . '/image/' . $r[0]['resource-id'] . ']';
$_REQUEST['body'] .= '[img=' . DI::baseUrl() . '/photo/' . $r[0]['resource-id'] . '-' . $r[0]['scale'] . '.' . $ext . ']' . $description . '[/img][/url]';
}
}
}
// set this so that the item_post() function is quiet and doesn't redirect or emit json
$_REQUEST['api_source'] = true;
if (empty($_REQUEST['source'])) {
$_REQUEST["source"] = api_source();
}
// call out normal post function
$item_id = item_post($a);
// output the post that we just posted.
return api_status_show($type, $item_id);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/update', 'api_statuses_update', true, API_METHOD_POST);
api_register_func('api/statuses/update_with_media', 'api_statuses_update', true, API_METHOD_POST);
/**
* Uploads an image to Friendica.
*
* @return array
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload
*/
function api_media_upload()
{
$a = DI::app();
if (api_user() === false) {
Logger::log('no user');
throw new ForbiddenException();
}
api_get_user($a);
if (empty($_FILES['media'])) {
// Output error
throw new BadRequestException("No media.");
}
$media = wall_upload_post($a, false);
if (!$media) {
// Output error
throw new InternalServerErrorException();
}
$returndata = [];
$returndata["media_id"] = $media["id"];
$returndata["media_id_string"] = (string)$media["id"];
$returndata["size"] = $media["size"];
$returndata["image"] = ["w" => $media["width"],
"h" => $media["height"],
"image_type" => $media["type"],
"friendica_preview_url" => $media["preview"]];
Logger::log("Media uploaded: " . print_r($returndata, true), Logger::DEBUG);
return ["media" => $returndata];
}
/// @TODO move to top of file or somewhere better
api_register_func('api/media/upload', 'api_media_upload', true, API_METHOD_POST);
/**
* Updates media meta data (picture descriptions)
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws TooManyRequestsException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
*
* @todo Compare the corresponding Twitter function for correct return values
*/
function api_media_metadata_create($type)
{
$a = DI::app();
if (api_user() === false) {
Logger::info('no user');
throw new ForbiddenException();
}
api_get_user($a);
$postdata = Network::postdata();
if (empty($postdata)) {
throw new BadRequestException("No post data");
}
$data = json_decode($postdata, true);
if (empty($data)) {
throw new BadRequestException("Invalid post data");
}
if (empty($data['media_id']) || empty($data['alt_text'])) {
throw new BadRequestException("Missing post data values");
}
if (empty($data['alt_text']['text'])) {
throw new BadRequestException("No alt text.");
}
Logger::info('Updating metadata', ['media_id' => $data['media_id']]);
$condition = ['id' => $data['media_id'], 'uid' => api_user()];
$photo = DBA::selectFirst('photo', ['resource-id'], $condition);
if (!DBA::isResult($photo)) {
throw new BadRequestException("Metadata not found.");
}
DBA::update('photo', ['desc' => $data['alt_text']['text']], ['resource-id' => $photo['resource-id']]);
}
api_register_func('api/media/metadata/create', 'api_media_metadata_create', true, API_METHOD_POST);
/**
* @param string $type Return format (atom, rss, xml, json)
* @param int $item_id
* @return string
* @throws Exception
*/
function api_status_show($type, $item_id)
{
Logger::info(API_LOG_PREFIX . 'Start', ['action' => 'status_show', 'type' => $type, 'item_id' => $item_id]);
$status_info = [];
$item = api_get_item(['id' => $item_id]);
if (!empty($item)) {
$status_info = api_format_item($item, $type);
}
Logger::info(API_LOG_PREFIX . 'End', ['action' => 'get_status', 'status_info' => $status_info]);
return api_format_data('statuses', $type, ['status' => $status_info]);
}
/**
* Retrieves the last public status of the provided user info
*
* @param int $ownerId Public contact Id
* @param int $uid User Id
* @return array
* @throws Exception
*/
function api_get_last_status($ownerId, $uid)
{
$condition = [
'author-id'=> $ownerId,
'uid' => $uid,
'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
'private' => [Item::PUBLIC, Item::UNLISTED]
];
$item = api_get_item($condition);
return $item;
}
/**
* Retrieves a single item record based on the provided condition and converts it for API use.
*
* @param array $condition Item table condition array
* @return array
* @throws Exception
*/
function api_get_item(array $condition)
{
$item = Item::selectFirst(Item::DISPLAY_FIELDLIST, $condition, ['order' => ['id' => true]]);
return $item;
}
/**
* Returns extended information of a given user, specified by ID or screen name as per the required id parameter.
* The author's most recent status will be returned inline.
*
* @param string $type Return type (atom, rss, xml, json)
* @return array|string
* @throws BadRequestException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show
*/
function api_users_show($type)
{
$a = Friendica\DI::app();
$user_info = api_get_user($a);
$item = api_get_last_status($user_info['pid'], $user_info['uid']);
if (!empty($item)) {
$user_info['status'] = api_format_item($item, $type);
}
// "uid" and "self" are only needed for some internal stuff, so remove it from here
unset($user_info['uid']);
unset($user_info['self']);
return api_format_data('user', $type, ['user' => $user_info]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/users/show', 'api_users_show');
api_register_func('api/externalprofile/show', 'api_users_show');
/**
* Search a public user account.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-search
*/
function api_users_search($type)
{
$a = DI::app();
$userlist = [];
if (!empty($_GET['q'])) {
$contacts = Contact::selectToArray(
['id'],
[
'`uid` = 0 AND (`name` = ? OR `nick` = ? OR `url` = ? OR `addr` = ?)',
$_GET['q'],
$_GET['q'],
$_GET['q'],
$_GET['q'],
]
);
if (DBA::isResult($contacts)) {
$k = 0;
foreach ($contacts as $contact) {
$user_info = api_get_user($a, $contact['id']);
if ($type == 'xml') {
$userlist[$k++ . ':user'] = $user_info;
} else {
$userlist[] = $user_info;
}
}
$userlist = ['users' => $userlist];
} else {
throw new NotFoundException('User ' . $_GET['q'] . ' not found.');
}
} else {
throw new BadRequestException('No search term specified.');
}
return api_format_data('users', $type, $userlist);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/users/search', 'api_users_search');
/**
* Return user objects
*
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup
*
* @param string $type Return format: json or xml
*
* @return array|string
* @throws BadRequestException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws NotFoundException if the results are empty.
* @throws UnauthorizedException
*/
function api_users_lookup($type)
{
$users = [];
if (!empty($_REQUEST['user_id'])) {
foreach (explode(',', $_REQUEST['user_id']) as $id) {
if (!empty($id)) {
$users[] = api_get_user(DI::app(), $id);
}
}
}
if (empty($users)) {
throw new NotFoundException;
}
return api_format_data("users", $type, ['users' => $users]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/users/lookup', 'api_users_lookup', true);
/**
* Returns statuses that match a specified query.
*
* @see https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets
*
* @param string $type Return format: json, xml, atom, rss
*
* @return array|string
* @throws BadRequestException if the "q" parameter is missing.
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_search($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
if (empty($_REQUEST['q'])) {
throw new BadRequestException('q parameter is required.');
}
$searchTerm = trim(rawurldecode($_REQUEST['q']));
$data = [];
$data['status'] = [];
$count = 15;
$exclude_replies = !empty($_REQUEST['exclude_replies']);
if (!empty($_REQUEST['rpp'])) {
$count = $_REQUEST['rpp'];
} elseif (!empty($_REQUEST['count'])) {
$count = $_REQUEST['count'];
}
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
$page = $_REQUEST['page'] ?? 1;
$start = max(0, ($page - 1) * $count);
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
if (preg_match('/^#(\w+)$/', $searchTerm, $matches) === 1 && isset($matches[1])) {
$searchTerm = $matches[1];
$condition = ["`oid` > ?
AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))
AND `otype` = ? AND `type` = ? AND `term` = ?",
$since_id, local_user(), TERM_OBJ_POST, TERM_HASHTAG, $searchTerm];
if ($max_id > 0) {
$condition[0] .= ' AND `oid` <= ?';
$condition[] = $max_id;
}
$terms = DBA::select('term', ['oid'], $condition, []);
$itemIds = [];
while ($term = DBA::fetch($terms)) {
$itemIds[] = $term['oid'];
}
DBA::close($terms);
if (empty($itemIds)) {
return api_format_data('statuses', $type, $data);
}
$preCondition = ['`id` IN (' . implode(', ', $itemIds) . ')'];
if ($exclude_replies) {
$preCondition[] = '`id` = `parent`';
}
$condition = [implode(' AND ', $preCondition)];
} else {
$condition = ["`id` > ?
" . ($exclude_replies ? " AND `id` = `parent` " : ' ') . "
AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))
AND `body` LIKE CONCAT('%',?,'%')",
$since_id, api_user(), $_REQUEST['q']];
if ($max_id > 0) {
$condition[0] .= ' AND `id` <= ?';
$condition[] = $max_id;
}
}
$statuses = [];
if (parse_url($searchTerm, PHP_URL_SCHEME) != '') {
$id = Item::fetchByLink($searchTerm, api_user());
if (!$id) {
// Public post
$id = Item::fetchByLink($searchTerm);
}
if (!empty($id)) {
$statuses = Item::select([], ['id' => $id]);
}
}
$statuses = $statuses ?: Item::selectForUser(api_user(), [], $condition, $params);
$data['status'] = api_format_items(Item::inArray($statuses), $user_info);
bindComments($data['status']);
return api_format_data('statuses', $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/search/tweets', 'api_search', true);
api_register_func('api/search', 'api_search', true);
/**
* Returns the most recent statuses posted by the user and the users they follow.
*
* @see https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @todo Optional parameters
* @todo Add reply info
*/
function api_statuses_home_timeline($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
unset($_REQUEST["screen_name"]);
unset($_GET["screen_name"]);
// get last network messages
// params
$count = $_REQUEST['count'] ?? 20;
$page = $_REQUEST['page']?? 0;
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
$exclude_replies = !empty($_REQUEST['exclude_replies']);
$conversation_id = $_REQUEST['conversation_id'] ?? 0;
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `item`.`id` > ?",
api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
if ($max_id > 0) {
$condition[0] .= " AND `item`.`id` <= ?";
$condition[] = $max_id;
}
if ($exclude_replies) {
$condition[0] .= ' AND `item`.`parent` = `item`.`id`';
}
if ($conversation_id > 0) {
$condition[0] .= " AND `item`.`parent` = ?";
$condition[] = $conversation_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
$items = Item::inArray($statuses);
$ret = api_format_items($items, $user_info, false, $type);
// Set all posts from the query above to seen
$idarray = [];
foreach ($items as $item) {
$idarray[] = intval($item["id"]);
}
if (!empty($idarray)) {
$unseen = Item::exists(['unseen' => true, 'id' => $idarray]);
if ($unseen) {
Item::update(['unseen' => false], ['unseen' => true, 'id' => $idarray]);
}
}
bindComments($ret);
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/home_timeline', 'api_statuses_home_timeline', true);
api_register_func('api/statuses/friends_timeline', 'api_statuses_home_timeline', true);
/**
* Returns the most recent statuses from public users.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_statuses_public_timeline($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
// get last network messages
// params
$count = $_REQUEST['count'] ?? 20;
$page = $_REQUEST['page'] ?? 1;
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
$exclude_replies = (!empty($_REQUEST['exclude_replies']) ? 1 : 0);
$conversation_id = $_REQUEST['conversation_id'] ?? 0;
$start = max(0, ($page - 1) * $count);
if ($exclude_replies && !$conversation_id) {
$condition = ["`gravity` IN (?, ?) AND `iid` > ? AND `private` = ? AND `wall` AND NOT `author`.`hidden`",
GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
if ($max_id > 0) {
$condition[0] .= " AND `thread`.`iid` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['iid' => true], 'limit' => [$start, $count]];
$statuses = Item::selectThreadForUser(api_user(), Item::DISPLAY_FIELDLIST, $condition, $params);
$r = Item::inArray($statuses);
} else {
$condition = ["`gravity` IN (?, ?) AND `id` > ? AND `private` = ? AND `wall` AND `item`.`origin` AND NOT `author`.`hidden`",
GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
if ($max_id > 0) {
$condition[0] .= " AND `item`.`id` <= ?";
$condition[] = $max_id;
}
if ($conversation_id > 0) {
$condition[0] .= " AND `item`.`parent` = ?";
$condition[] = $conversation_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
$r = Item::inArray($statuses);
}
$ret = api_format_items($r, $user_info, false, $type);
bindComments($ret);
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/public_timeline', 'api_statuses_public_timeline', true);
/**
* Returns the most recent statuses posted by users this node knows about.
*
* @param string $type Return format: json, xml, atom, rss
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_statuses_networkpublic_timeline($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
// pagination
$count = $_REQUEST['count'] ?? 20;
$page = $_REQUEST['page'] ?? 1;
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = 0 AND `gravity` IN (?, ?) AND `thread`.`iid` > ? AND `private` = ?",
GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, Item::PUBLIC];
if ($max_id > 0) {
$condition[0] .= " AND `thread`.`iid` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['iid' => true], 'limit' => [$start, $count]];
$statuses = Item::selectThreadForUser(api_user(), Item::DISPLAY_FIELDLIST, $condition, $params);
$ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
bindComments($ret);
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/networkpublic_timeline', 'api_statuses_networkpublic_timeline', true);
/**
* Returns a single status.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id
*/
function api_statuses_show($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
// params
$id = intval($a->argv[3] ?? 0);
if ($id == 0) {
$id = intval($_REQUEST['id'] ?? 0);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4] ?? 0);
}
Logger::log('API: api_statuses_show: ' . $id);
$conversation = !empty($_REQUEST['conversation']);
// try to fetch the item for the local user - or the public item, if there is no local one
$uri_item = Item::selectFirst(['uri'], ['id' => $id]);
if (!DBA::isResult($uri_item)) {
throw new BadRequestException("There is no status with this id.");
}
$item = Item::selectFirst(['id'], ['uri' => $uri_item['uri'], 'uid' => [0, api_user()]], ['order' => ['uid' => true]]);
if (!DBA::isResult($item)) {
throw new BadRequestException("There is no status with this id.");
}
$id = $item['id'];
if ($conversation) {
$condition = ['parent' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
$params = ['order' => ['id' => true]];
} else {
$condition = ['id' => $id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]];
$params = [];
}
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
/// @TODO How about copying this to above methods which don't check $r ?
if (!DBA::isResult($statuses)) {
throw new BadRequestException("There is no status with this id.");
}
$ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
if ($conversation) {
$data = ['status' => $ret];
return api_format_data("statuses", $type, $data);
} else {
$data = ['status' => $ret[0]];
return api_format_data("status", $type, $data);
}
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/show', 'api_statuses_show', true);
/**
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @todo nothing to say?
*/
function api_conversation_show($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
// params
$id = intval($a->argv[3] ?? 0);
$since_id = intval($_REQUEST['since_id'] ?? 0);
$max_id = intval($_REQUEST['max_id'] ?? 0);
$count = intval($_REQUEST['count'] ?? 20);
$page = intval($_REQUEST['page'] ?? 1);
$start = max(0, ($page - 1) * $count);
if ($id == 0) {
$id = intval($_REQUEST['id'] ?? 0);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4] ?? 0);
}
Logger::info(API_LOG_PREFIX . '{subaction}', ['module' => 'api', 'action' => 'conversation', 'subaction' => 'show', 'id' => $id]);
// try to fetch the item for the local user - or the public item, if there is no local one
$item = Item::selectFirst(['parent-uri'], ['id' => $id]);
if (!DBA::isResult($item)) {
throw new BadRequestException("There is no status with this id.");
}
$parent = Item::selectFirst(['id'], ['uri' => $item['parent-uri'], 'uid' => [0, api_user()]], ['order' => ['uid' => true]]);
if (!DBA::isResult($parent)) {
throw new BadRequestException("There is no status with this id.");
}
$id = $parent['id'];
$condition = ["`parent` = ? AND `uid` IN (0, ?) AND `gravity` IN (?, ?) AND `item`.`id` > ?",
$id, api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
if ($max_id > 0) {
$condition[0] .= " AND `item`.`id` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
if (!DBA::isResult($statuses)) {
throw new BadRequestException("There is no status with id $id.");
}
$ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
$data = ['status' => $ret];
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/conversation/show', 'api_conversation_show', true);
api_register_func('api/statusnet/conversation', 'api_conversation_show', true);
/**
* Repeats a status.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id
*/
function api_statuses_repeat($type)
{
global $called_api;
$a = DI::app();
if (api_user() === false) {
throw new ForbiddenException();
}
api_get_user($a);
// params
$id = intval($a->argv[3] ?? 0);
if ($id == 0) {
$id = intval($_REQUEST['id'] ?? 0);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4] ?? 0);
}
Logger::log('API: api_statuses_repeat: '.$id);
$fields = ['body', 'title', 'attach', 'tag', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink'];
$item = Item::selectFirst($fields, ['id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED]]);
if (DBA::isResult($item) && $item['body'] != "") {
if (strpos($item['body'], "[/share]") !== false) {
$pos = strpos($item['body'], "[share");
$post = substr($item['body'], $pos);
} else {
$post = share_header($item['author-name'], $item['author-link'], $item['author-avatar'], $item['guid'], $item['created'], $item['plink']);
if (!empty($item['title'])) {
$post .= '[h3]' . $item['title'] . "[/h3]\n";
}
$post .= $item['body'];
$post .= "[/share]";
}
$_REQUEST['body'] = $post;
$_REQUEST['tag'] = $item['tag'];
$_REQUEST['attach'] = $item['attach'];
$_REQUEST['profile_uid'] = api_user();
$_REQUEST['api_source'] = true;
if (empty($_REQUEST['source'])) {
$_REQUEST["source"] = api_source();
}
$item_id = item_post($a);
} else {
throw new ForbiddenException();
}
// output the post that we just posted.
$called_api = [];
return api_status_show($type, $item_id);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/retweet', 'api_statuses_repeat', true, API_METHOD_POST);
/**
* Destroys a specific status.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id
*/
function api_statuses_destroy($type)
{
$a = DI::app();
if (api_user() === false) {
throw new ForbiddenException();
}
api_get_user($a);
// params
$id = intval($a->argv[3] ?? 0);
if ($id == 0) {
$id = intval($_REQUEST['id'] ?? 0);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4] ?? 0);
}
Logger::log('API: api_statuses_destroy: '.$id);
$ret = api_statuses_show($type);
Item::deleteForUser(['id' => $id], api_user());
return $ret;
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/destroy', 'api_statuses_destroy', true, API_METHOD_DELETE);
/**
* Returns the most recent mentions.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see http://developer.twitter.com/doc/get/statuses/mentions
*/
function api_statuses_mentions($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
unset($_REQUEST["screen_name"]);
unset($_GET["screen_name"]);
// get last network messages
// params
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
$count = $_REQUEST['count'] ?? 20;
$page = $_REQUEST['page'] ?? 1;
$start = max(0, ($page - 1) * $count);
$query = "SELECT `item`.`id` FROM `user-item`
INNER JOIN `item` ON `item`.`id` = `user-item`.`iid` AND `item`.`gravity` IN (?, ?)
WHERE (`user-item`.`hidden` IS NULL OR NOT `user-item`.`hidden`) AND
`user-item`.`uid` = ? AND `user-item`.`notification-type` & ? != 0
AND `user-item`.`iid` > ?";
$condition = [GRAVITY_PARENT, GRAVITY_COMMENT, api_user(),
UserItem::NOTIF_EXPLICIT_TAGGED | UserItem::NOTIF_IMPLICIT_TAGGED |
UserItem::NOTIF_THREAD_COMMENT | UserItem::NOTIF_DIRECT_COMMENT |
UserItem::NOTIF_DIRECT_THREAD_COMMENT,
$since_id];
if ($max_id > 0) {
$query .= " AND `item`.`id` <= ?";
$condition[] = $max_id;
}
$query .= " ORDER BY `user-item`.`iid` DESC LIMIT ?, ?";
$condition[] = $start;
$condition[] = $count;
$useritems = DBA::p($query, $condition);
$itemids = [];
while ($useritem = DBA::fetch($useritems)) {
$itemids[] = $useritem['id'];
}
DBA::close($useritems);
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$statuses = Item::selectForUser(api_user(), [], ['id' => $itemids], $params);
$ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/mentions', 'api_statuses_mentions', true);
api_register_func('api/statuses/replies', 'api_statuses_mentions', true);
/**
* Returns the most recent statuses posted by the user.
*
* @param string $type Either "json" or "xml"
* @return string|array
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline
*/
function api_statuses_user_timeline($type)
{
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
Logger::log(
"api_statuses_user_timeline: api_user: ". api_user() .
"\nuser_info: ".print_r($user_info, true) .
"\n_REQUEST: ".print_r($_REQUEST, true),
Logger::DEBUG
);
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
$exclude_replies = !empty($_REQUEST['exclude_replies']);
$conversation_id = $_REQUEST['conversation_id'] ?? 0;
// pagination
$count = $_REQUEST['count'] ?? 20;
$page = $_REQUEST['page'] ?? 1;
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `item`.`id` > ? AND `item`.`contact-id` = ?",
api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, $user_info['cid']];
if ($user_info['self'] == 1) {
$condition[0] .= ' AND `item`.`wall` ';
}
if ($exclude_replies) {
$condition[0] .= ' AND `item`.`parent` = `item`.`id`';
}
if ($conversation_id > 0) {
$condition[0] .= " AND `item`.`parent` = ?";
$condition[] = $conversation_id;
}
if ($max_id > 0) {
$condition[0] .= " AND `item`.`id` <= ?";
$condition[] = $max_id;
}
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
$ret = api_format_items(Item::inArray($statuses), $user_info, true, $type);
bindComments($ret);
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/user_timeline', 'api_statuses_user_timeline', true);
/**
* Star/unstar an item.
* param: id : id of the item
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://web.archive.org/web/20131019055350/https://dev.twitter.com/docs/api/1/post/favorites/create/%3Aid
*/
function api_favorites_create_destroy($type)
{
$a = DI::app();
if (api_user() === false) {
throw new ForbiddenException();
}
// for versioned api.
/// @TODO We need a better global soluton
$action_argv_id = 2;
if (count($a->argv) > 1 && $a->argv[1] == "1.1") {
$action_argv_id = 3;
}
if ($a->argc <= $action_argv_id) {
throw new BadRequestException("Invalid request.");
}
$action = str_replace("." . $type, "", $a->argv[$action_argv_id]);
if ($a->argc == $action_argv_id + 2) {
$itemid = intval($a->argv[$action_argv_id + 1] ?? 0);
} else {
$itemid = intval($_REQUEST['id'] ?? 0);
}
$item = Item::selectFirstForUser(api_user(), [], ['id' => $itemid, 'uid' => api_user()]);
if (!DBA::isResult($item)) {
throw new BadRequestException("Invalid item.");
}
switch ($action) {
case "create":
$item['starred'] = 1;
break;
case "destroy":
$item['starred'] = 0;
break;
default:
throw new BadRequestException("Invalid action ".$action);
}
$r = Item::update(['starred' => $item['starred']], ['id' => $itemid]);
if ($r === false) {
throw new InternalServerErrorException("DB error");
}
$user_info = api_get_user($a);
$rets = api_format_items([$item], $user_info, false, $type);
$ret = $rets[0];
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("status", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/favorites/create', 'api_favorites_create_destroy', true, API_METHOD_POST);
api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true, API_METHOD_DELETE);
/**
* Returns the most recent favorite statuses.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return string|array
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
*/
function api_favorites($type)
{
global $called_api;
$a = DI::app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
$called_api = [];
// in friendica starred item are private
// return favorites only for self
Logger::info(API_LOG_PREFIX . 'for {self}', ['module' => 'api', 'action' => 'favorites', 'self' => $user_info['self']]);
if ($user_info['self'] == 0) {
$ret = [];
} else {
// params
$since_id = $_REQUEST['since_id'] ?? 0;
$max_id = $_REQUEST['max_id'] ?? 0;
$count = $_GET['count'] ?? 20;
$page = $_REQUEST['page'] ?? 1;
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `id` > ? AND `starred`",
api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id];
$params = ['order' => ['id' => true], 'limit' => [$start, $count]];
if ($max_id > 0) {
$condition[0] .= " AND `item`.`id` <= ?";
$condition[] = $max_id;
}
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
$ret = api_format_items(Item::inArray($statuses), $user_info, false, $type);
}
bindComments($ret);
$data = ['status' => $ret];
switch ($type) {
case "atom":
break;
case "rss":
$data = api_rss_extra($a, $data, $user_info);
break;
}
return api_format_data("statuses", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/favorites', 'api_favorites', true);
/**
*
* @param array $item
* @param array $recipient
* @param array $sender
*
* @return array
* @throws InternalServerErrorException
*/
function api_format_messages($item, $recipient, $sender)
{
// standard meta information
$ret = [
'id' => $item['id'],
'sender_id' => $sender['id'],
'text' => "",
'recipient_id' => $recipient['id'],
'created_at' => api_date($item['created'] ?? DateTimeFormat::utcNow()),
'sender_screen_name' => $sender['screen_name'],
'recipient_screen_name' => $recipient['screen_name'],
'sender' => $sender,
'recipient' => $recipient,
'title' => "",
'friendica_seen' => $item['seen'] ?? 0,
'friendica_parent_uri' => $item['parent-uri'] ?? '',
];
// "uid" and "self" are only needed for some internal stuff, so remove it from here
if (isset($ret['sender']['uid'])) {
unset($ret['sender']['uid']);
}
if (isset($ret['sender']['self'])) {
unset($ret['sender']['self']);
}
if (isset($ret['recipient']['uid'])) {
unset($ret['recipient']['uid']);
}
if (isset($ret['recipient']['self'])) {
unset($ret['recipient']['self']);
}
//don't send title to regular StatusNET requests to avoid confusing these apps
if (!empty($_GET['getText'])) {
$ret['title'] = $item['title'];
if ($_GET['getText'] == 'html') {
$ret['text'] = BBCode::convert($item['body'], false);
} elseif ($_GET['getText'] == 'plain') {
$ret['text'] = trim(HTML::toPlaintext(BBCode::convert(api_clean_plain_items($item['body']), false, 2, true), 0));
}
} else {
$ret['text'] = $item['title'] . "\n" . HTML::toPlaintext(BBCode::convert(api_clean_plain_items($item['body']), false, 2, true), 0);
}
if (!empty($_GET['getUserObjects']) && $_GET['getUserObjects'] == 'false') {
unset($ret['sender']);