1
1
Fork 0
friendica_2019-12_sharedHos.../include/api.php
Pierre Rudloff 036803d8c7 Typo
2018-04-07 22:37:57 +02:00

6257 lines
176 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
/**
* 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\Feature;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\Addon;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\NotificationsManager;
use Friendica\Core\PConfig;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBM;
use Friendica\Model\Contact;
use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\Mail;
use Friendica\Model\Photo;
use Friendica\Model\User;
use Friendica\Network\FKOAuth1;
use Friendica\Network\HTTPException;
use Friendica\Network\HTTPException\BadRequestException;
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\Diaspora;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use Friendica\Util\XML;
require_once 'include/conversation.php';
require_once 'mod/share.php';
require_once 'mod/item.php';
require_once 'include/security.php';
require_once 'mod/wall_upload.php';
require_once 'mod/proxy.php';
define('API_METHOD_ANY', '*');
define('API_METHOD_GET', 'GET');
define('API_METHOD_POST', 'POST,PUT');
define('API_METHOD_DELETE', 'POST,DELETE');
$API = [];
$called_api = null;
/**
* 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).
*
* @brief Auth API user
*/
function api_user()
{
if (x($_SESSION, 'allow_api')) {
return local_user();
}
return false;
}
/**
* 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)
*
* @brief Get source name from API client
*
* @return string
* Client source name, default to "api" if unset/unknown
*/
function api_source()
{
if (requestdata('source')) {
return requestdata('source');
}
// Support for known clients that doesn't send a source name
if (strpos($_SERVER['HTTP_USER_AGENT'], "Twidere") !== false) {
return "Twidere";
}
logger("Unrecognized user-agent ".$_SERVER['HTTP_USER_AGENT'], LOGGER_DEBUG);
return "api";
}
/**
* @brief 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"
*/
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.
*
* @brief Register API endpoint
*
* @param string $path API URL path, relative to System::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
*
* @brief Login API user
*
* @param object $a App
* @hook 'authenticate'
* array $addon_auth
* 'username' => username from login form
* 'password' => password from login form
* 'authenticated' => return status,
* 'user_record' => return authenticated user record
* @hook 'logged_in'
* array $user logged user record
*/
function api_login(App $a)
{
$oauth1 = new FKOAuth1();
// login with oauth
try {
list($consumer, $token) = $oauth1->verify_request(OAuthRequest::from_request());
if (!is_null($token)) {
$oauth1->loginUser($token->uid);
Addon::callHooks('logged_in', $a->user);
return;
}
echo __FILE__.__LINE__.__FUNCTION__ . "<pre>";
var_dump($consumer, $token);
die();
} catch (Exception $e) {
logger($e);
}
// workaround for HTTP-auth in CGI mode
if (x($_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 (!x($_SERVER, 'PHP_AUTH_USER')) {
logger('API_login: ' . print_r($_SERVER, true), LOGGER_DEBUG);
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.
*/
Addon::callHooks('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));
if ($user_id) {
$record = dba::selectFirst('user', [], ['uid' => $user_id]);
}
}
if (!DBM::is_result($record)) {
logger('API_login failure: ' . print_r($_SERVER, true), LOGGER_DEBUG);
header('WWW-Authenticate: Basic realm="Friendica"');
//header('HTTP/1.0 401 Unauthorized');
//die('This api requires login');
throw new UnauthorizedException("This API requires login");
}
authenticate_success($record);
$_SESSION["allow_api"] = true;
Addon::callHooks('logged_in', $a->user);
}
/**
* API endpoints can define which HTTP method to accept when called.
* This function check the current HTTP method agains endpoint
* registered method.
*
* @brief Check HTTP method of called API
*
* @param string $method Required methods, uppercase, separated by comma
* @return bool
*/
function api_check_method($method)
{
if ($method == "*") {
return true;
}
return (strpos($method, $_SERVER['REQUEST_METHOD']) !== false);
}
/**
* Authenticate user, call registered API function, set HTTP headers
*
* @brief Main API entry point
*
* @param object $a App
* @return string API call result
*/
function api_call(App $a)
{
global $API, $called_api;
$type = "json";
if (strpos($a->query_string, ".xml") > 0) {
$type = "xml";
}
if (strpos($a->query_string, ".json") > 0) {
$type = "json";
}
if (strpos($a->query_string, ".rss") > 0) {
$type = "rss";
}
if (strpos($a->query_string, ".atom") > 0) {
$type = "atom";
}
try {
foreach ($API as $p => $info) {
if (strpos($a->query_string, $p) === 0) {
if (!api_check_method($info['method'])) {
throw new MethodNotAllowedException();
}
$called_api = explode("/", $p);
//unset($_SERVER['PHP_AUTH_USER']);
/// @TODO should be "true ==[=] $info['auth']", if you miss only one = character, you assign a variable (only with ==). Let's make all this even.
if ($info['auth'] === true && api_user() === false) {
api_login($a);
}
logger('API call for ' . $a->user['username'] . ': ' . $a->query_string);
logger('API parameters: ' . print_r($_REQUEST, true));
$stamp = microtime(true);
$return = call_user_func($info['func'], $type);
$duration = (float) (microtime(true) - $stamp);
logger("API call duration: " . round($duration, 2) . "\t" . $a->query_string, LOGGER_DEBUG);
if (Config::get("system", "profiler")) {
$duration = microtime(true)-$a->performance["start"];
/// @TODO round() really everywhere?
logger(
parse_url($a->query_string, PHP_URL_PATH) . ": " . sprintf(
"Database: %s/%s, Cache %s/%s, Network: %s, I/O: %s, Other: %s, Total: %s",
round($a->performance["database"] - $a->performance["database_write"], 3),
round($a->performance["database_write"], 3),
round($a->performance["cache"], 3),
round($a->performance["cache_write"], 3),
round($a->performance["network"], 2),
round($a->performance["file"], 2),
round($duration - ($a->performance["database"]
+ $a->performance["cache"] + $a->performance["cache_write"]
+ $a->performance["network"] + $a->performance["file"]), 2),
round($duration, 2)
),
LOGGER_DEBUG
);
if (Config::get("rendertime", "callstack")) {
$o = "Database Read:\n";
foreach ($a->callstack["database"] as $func => $time) {
$time = round($time, 3);
if ($time > 0) {
$o .= $func . ": " . $time . "\n";
}
}
$o .= "\nDatabase Write:\n";
foreach ($a->callstack["database_write"] as $func => $time) {
$time = round($time, 3);
if ($time > 0) {
$o .= $func . ": " . $time . "\n";
}
}
$o = "Cache Read:\n";
foreach ($a->callstack["cache"] as $func => $time) {
$time = round($time, 3);
if ($time > 0) {
$o .= $func . ": " . $time . "\n";
}
}
$o .= "\nCache Write:\n";
foreach ($a->callstack["cache_write"] as $func => $time) {
$time = round($time, 3);
if ($time > 0) {
$o .= $func . ": " . $time . "\n";
}
}
$o .= "\nNetwork:\n";
foreach ($a->callstack["network"] as $func => $time) {
$time = round($time, 3);
if ($time > 0) {
$o .= $func . ": " . $time . "\n";
}
}
logger($o, LOGGER_DEBUG);
}
}
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");
foreach ($return as $rr) {
$json = json_encode($rr);
}
if (x($_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('API call not implemented: ' . $a->query_string);
throw new NotImplementedException();
} catch (HTTPException $e) {
header("HTTP/1.1 {$e->httpcode} {$e->httpdesc}");
return api_error($type, $e);
}
}
/**
* @brief Format API error string
*
* @param string $type Return type (xml, json, rss, as)
* @param object $e HTTPException Error object
* @return string error message formatted as $type
*/
function api_error($type, $e)
{
$a = get_app();
$error = ($e->getMessage() !== "" ? $e->getMessage() : $e->httpdesc);
/// @TODO: https://dev.twitter.com/overview/api/response-codes
$error = ["error" => $error,
"code" => $e->httpcode . " " . $e->httpdesc,
"request" => $a->query_string];
$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;
}
/**
* @brief 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
* @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' => System::baseUrl() . "/" . $a->query_string,
'base' => System::baseUrl(),
'updated' => api_date(null),
'atom_updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'language' => $user_info['language'],
'logo' => System::baseUrl() . "/images/friendica-32.png",
];
return $arr;
}
/**
* @brief Unique contact to contact url.
*
* @param int $id Contact id
* @return bool|string
* Contact url or False if contact id is unknown
*/
function api_unique_id_to_nurl($id)
{
$r = dba::selectFirst('contact', ['nurl'], ['uid' => 0, 'id' => $id]);
if (DBM::is_result($r)) {
return $r["nurl"];
} else {
return false;
}
}
/**
* @brief Get user info array.
*
* @param object $a App
* @param int|string $contact_id Contact ID or URL
*/
function api_get_user(App $a, $contact_id = null)
{
global $called_api;
$user = null;
$extra_query = "";
$url = "";
logger("api_get_user: Fetching user data for user ".$contact_id, LOGGER_DEBUG);
// Searching for contact URL
if (!is_null($contact_id) && (intval($contact_id) == 0)) {
$user = dbesc(normalise_link($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 = dbesc(api_unique_id_to_nurl($contact_id));
if ($user == "") {
throw new BadRequestException("User 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) && x($_GET, 'user_id')) {
$user = dbesc(api_unique_id_to_nurl($_GET['user_id']));
if ($user == "") {
throw new BadRequestException("User 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) && x($_GET, 'screen_name')) {
$user = dbesc($_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) && x($_GET, 'profileurl')) {
$user = dbesc(normalise_link($_GET['profileurl']));
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=".intval(api_user());
}
}
if (is_null($user) && ($a->argc > (count($called_api) - 1)) && (count($called_api) > 0)) {
$argid = count($called_api);
list($user, $null) = explode(".", $a->argv[$argid]);
if (is_numeric($user)) {
$user = dbesc(api_unique_id_to_nurl($user));
if ($user == "") {
return false;
}
$url = $user;
$extra_query = "AND `contact`.`nurl` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
} else {
$user = dbesc($user);
$extra_query = "AND `contact`.`nick` = '%s' ";
if (api_user() !== false) {
$extra_query .= "AND `contact`.`uid`=" . intval(api_user());
}
}
}
logger("api_get_user: user ".$user, LOGGER_DEBUG);
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('api_user: ' . $extra_query . ', user: ' . $user);
// user info
$uinfo = q(
"SELECT *, `contact`.`id` AS `cid` FROM `contact`
WHERE 1
$extra_query",
$user
);
// Selecting the id by priority, friendica first
api_best_nickname($uinfo);
// if the contact wasn't found, fetch it from the contacts with uid = 0
if (!DBM::is_result($uinfo)) {
$r = [];
if ($url != "") {
$r = q("SELECT * FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s' LIMIT 1", dbesc(normalise_link($url)));
}
if (DBM::is_result($r)) {
$network_name = ContactSelector::networkToName($r[0]['network'], $r[0]['url']);
// If no nick where given, extract it from the address
if (($r[0]['nick'] == "") || ($r[0]['name'] == $r[0]['nick'])) {
$r[0]['nick'] = api_get_nick($r[0]["url"]);
}
$ret = [
'id' => $r[0]["id"],
'id_str' => (string) $r[0]["id"],
'name' => $r[0]["name"],
'screen_name' => (($r[0]['nick']) ? $r[0]['nick'] : $r[0]['name']),
'location' => ($r[0]["location"] != "") ? $r[0]["location"] : $network_name,
'description' => $r[0]["about"],
'profile_image_url' => $r[0]["micro"],
'profile_image_url_https' => $r[0]["micro"],
'profile_image_url_profile_size' => $r[0]["thumb"],
'profile_image_url_large' => $r[0]["photo"],
'url' => $r[0]["url"],
'protected' => false,
'followers_count' => 0,
'friends_count' => 0,
'listed_count' => 0,
'created_at' => api_date($r[0]["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' => $r[0]["url"],
'uid' => 0,
'cid' => Contact::getIdForURL($r[0]["url"], api_user(), true),
'self' => 0,
'network' => $r[0]["network"],
];
return $ret;
} else {
throw new BadRequestException("User not found.");
}
}
if ($uinfo[0]['self']) {
if ($uinfo[0]['network'] == "") {
$uinfo[0]['network'] = NETWORK_DFRN;
}
$usr = q(
"SELECT * FROM `user` WHERE `uid` = %d LIMIT 1",
intval(api_user())
);
$profile = q(
"SELECT * FROM `profile` WHERE `uid` = %d AND `is-default` = 1 LIMIT 1",
intval(api_user())
);
/// @TODO old-lost code? (twice)
// Counting is deactivated by now, due to performance issues
// count public wall messages
//$r = q("SELECT COUNT(*) as `count` FROM `item` WHERE `uid` = %d AND `wall`",
// intval($uinfo[0]['uid'])
//);
//$countitms = $r[0]['count'];
$countitms = 0;
} else {
// Counting is deactivated by now, due to performance issues
//$r = q("SELECT count(*) as `count` FROM `item`
// WHERE `contact-id` = %d",
// intval($uinfo[0]['id'])
//);
//$countitms = $r[0]['count'];
$countitms = 0;
}
/// @TODO old-lost code? (twice)
/*
// Counting is deactivated by now, due to performance issues
// count friends
$r = q("SELECT count(*) as `count` FROM `contact`
WHERE `uid` = %d AND `rel` IN ( %d, %d )
AND `self`=0 AND NOT `blocked` AND NOT `pending` AND `hidden`=0",
intval($uinfo[0]['uid']),
intval(CONTACT_IS_SHARING),
intval(CONTACT_IS_FRIEND)
);
$countfriends = $r[0]['count'];
$r = q("SELECT count(*) as `count` FROM `contact`
WHERE `uid` = %d AND `rel` IN ( %d, %d )
AND `self`=0 AND NOT `blocked` AND NOT `pending` AND `hidden`=0",
intval($uinfo[0]['uid']),
intval(CONTACT_IS_FOLLOWER),
intval(CONTACT_IS_FRIEND)
);
$countfollowers = $r[0]['count'];
$r = q("SELECT count(*) as `count` FROM item where starred = 1 and uid = %d and deleted = 0",
intval($uinfo[0]['uid'])
);
$starred = $r[0]['count'];
if (! $uinfo[0]['self']) {
$countfriends = 0;
$countfollowers = 0;
$starred = 0;
}
*/
$countfriends = 0;
$countfollowers = 0;
$starred = 0;
// Add a nick if it isn't present there
if (($uinfo[0]['nick'] == "") || ($uinfo[0]['name'] == $uinfo[0]['nick'])) {
$uinfo[0]['nick'] = api_get_nick($uinfo[0]["url"]);
}
$network_name = ContactSelector::networkToName($uinfo[0]['network'], $uinfo[0]['url']);
$pcontact_id = Contact::getIdForURL($uinfo[0]['url'], 0, true);
if (!empty($profile[0]['about'])) {
$description = $profile[0]['about'];
} else {
$description = $uinfo[0]["about"];
}
if (!empty($usr[0]['default-location'])) {
$location = $usr[0]['default-location'];
} elseif (!empty($uinfo[0]["location"])) {
$location = $uinfo[0]["location"];
} else {
$location = $network_name;
}
$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' => $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($countitms),
'lang' => '',
'contributors_enabled' => false,
'is_translator' => false,
'is_translation_enabled' => false,
'following' => (($uinfo[0]['rel'] == CONTACT_IS_FOLLOWER) || ($uinfo[0]['rel'] == CONTACT_IS_FRIEND)),
'follow_request_sent' => false,
'statusnet_blocking' => false,
'notifications' => false,
/// @TODO old way?
//'statusnet_profile_url' => System::baseUrl()."/contacts/".$uinfo[0]['cid'],
'statusnet_profile_url' => $uinfo[0]['url'],
'uid' => intval($uinfo[0]['uid']),
'cid' => intval($uinfo[0]['cid']),
'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 = 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 = PConfig::get($ret['uid'], 'frio', 'nav_bg');
$link_color = PConfig::get($ret['uid'], 'frio', 'link_color');
$bgcolor = PConfig::get($ret['uid'], 'frio', 'background_color');
}
if (!$nav_bg) {
$nav_bg = "#708fa0";
}
if (!$link_color) {
$link_color = "#6fdbe8";
}
if (!$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;
}
/**
* @brief return api-formatted array for item's author and owner
*
* @param object $a App
* @param array $item item from db
* @return array(array:author, array:owner)
*/
function api_item_get_user(App $a, $item)
{
$status_user = api_get_user($a, $item["author-link"]);
$status_user["protected"] = (($item["allow_cid"] != "") ||
($item["allow_gid"] != "") ||
($item["deny_cid"] != "") ||
($item["deny_gid"] != "") ||
$item["private"]);
if ($item['thr-parent'] == $item['uri']) {
$owner_user = api_get_user($a, $item["owner-link"]);
} else {
$owner_user = $status_user;
}
return ([$status_user, $owner_user]);
}
/**
* @brief walks recursively through an array with the possibility to change value and key
*
* @param array $array The array to walk through
* @param string $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;
}
/**
* @brief 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;
}
/**
* @brief 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($data, $root_element)
{
$childname = key($data);
$data2 = array_pop($data);
$key = key($data2);
$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)) {
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;
}
/**
* @brief 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 (string|object|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":
$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)
*/
function api_account_verify_credentials($type)
{
$a = get_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 = (x($_REQUEST, '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) {
$user_info["status"] = api_status_show("raw");
if (!count($user_info["status"])) {
unset($user_info["status"]);
} else {
unset($user_info["status"]["user"]);
}
}
// "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
*/
function requestdata($k)
{
if (x($_POST, $k)) {
return $_POST[$k];
}
if (x($_GET, $k)) {
return $_GET[$k];
}
return null;
}
/**
* Waitman Gobble Mod
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
*/
function api_statuses_mediap($type)
{
$a = get_app();
if (api_user() === false) {
logger('api_statuses_update: no user');
throw new ForbiddenException();
}
$user_info = api_get_user($a);
$_REQUEST['type'] = 'wall';
$_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 = html2bb_video($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?
// tell wall_upload function to return img info instead of echo
$_REQUEST['hush'] = 'yeah';
$bebop = wall_upload_post($a);
// 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" . $bebop;
item_post($a);
// this should output the last post (the one we just posted).
return api_status_show($type);
}
/// @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
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
*/
function api_statuses_update($type)
{
$a = get_app();
if (api_user() === false) {
logger('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 = html2bb_video($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) {
$_REQUEST['type'] = 'net-comment';
} else {
// Check for throttling (maximum posts per day, week and month)
$throttle_day = Config::get('system', 'throttle_limit_day');
if ($throttle_day > 0) {
$datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60);
$r = q(
"SELECT COUNT(*) AS `posts_day` FROM `item` WHERE `uid`=%d AND `wall`
AND `created` > '%s' AND `id` = `parent`",
intval(api_user()),
dbesc($datefrom)
);
if (DBM::is_result($r)) {
$posts_day = $r[0]["posts_day"];
} else {
$posts_day = 0;
}
if ($posts_day > $throttle_day) {
logger('Daily posting limit reached for user '.api_user(), LOGGER_DEBUG);
// die(api_error($type, L10n::t("Daily posting limit of %d posts reached. The post was rejected.", $throttle_day));
throw new TooManyRequestsException(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 = Config::get('system', 'throttle_limit_week');
if ($throttle_week > 0) {
$datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*7);
$r = q(
"SELECT COUNT(*) AS `posts_week` FROM `item` WHERE `uid`=%d AND `wall`
AND `created` > '%s' AND `id` = `parent`",
intval(api_user()),
dbesc($datefrom)
);
if (DBM::is_result($r)) {
$posts_week = $r[0]["posts_week"];
} else {
$posts_week = 0;
}
if ($posts_week > $throttle_week) {
logger('Weekly posting limit reached for user '.api_user(), LOGGER_DEBUG);
// die(api_error($type, L10n::t("Weekly posting limit of %d posts reached. The post was rejected.", $throttle_week)));
throw new TooManyRequestsException(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 = Config::get('system', 'throttle_limit_month');
if ($throttle_month > 0) {
$datefrom = date(DateTimeFormat::MYSQL, time() - 24*60*60*30);
$r = q(
"SELECT COUNT(*) AS `posts_month` FROM `item` WHERE `uid`=%d AND `wall`
AND `created` > '%s' AND `id` = `parent`",
intval(api_user()),
dbesc($datefrom)
);
if (DBM::is_result($r)) {
$posts_month = $r[0]["posts_month"];
} else {
$posts_month = 0;
}
if ($posts_month > $throttle_month) {
logger('Monthly posting limit reached for user '.api_user(), LOGGER_DEBUG);
// die(api_error($type, L10n::t("Monthly posting limit of %d posts reached. The post was rejected.", $throttle_month));
throw new TooManyRequestsException(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));
}
}
$_REQUEST['type'] = 'wall';
}
if (x($_FILES, 'media')) {
// upload the image if we have one
$_REQUEST['hush'] = 'yeah'; //tell wall_upload function to return img info instead of echo
$media = wall_upload_post($a);
if (strlen($media) > 0) {
$_REQUEST['body'] .= "\n\n" . $media;
}
}
// To-Do: Multiple IDs
if (requestdata('media_ids')) {
$r = q(
"SELECT `resource-id`, `scale`, `nickname`, `type` 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(requestdata('media_ids')),
api_user()
);
if (DBM::is_result($r)) {
$phototypes = Image::supportedTypes();
$ext = $phototypes[$r[0]['type']];
$_REQUEST['body'] .= "\n\n" . '[url=' . System::baseUrl() . '/photos/' . $r[0]['nickname'] . '/image/' . $r[0]['resource-id'] . ']';
$_REQUEST['body'] .= '[img]' . System::baseUrl() . '/photo/' . $r[0]['resource-id'] . '-' . $r[0]['scale'] . '.' . $ext . '[/img][/url]';
}
}
// set this so that the item_post() function is quiet and doesn't redirect or emit json
$_REQUEST['api_source'] = true;
if (!x($_REQUEST, "source")) {
$_REQUEST["source"] = api_source();
}
// call out normal post function
item_post($a);
// this should output the last post (the one we just posted).
return api_status_show($type);
}
/// @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
* @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload
*/
function api_media_upload()
{
$a = get_app();
if (api_user() === false) {
logger('no user');
throw new ForbiddenException();
}
api_get_user($a);
if (!x($_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"]];
logger("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);
/**
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
*/
function api_status_show($type)
{
$a = get_app();
$user_info = api_get_user($a);
logger('api_status_show: user_info: '.print_r($user_info, true), LOGGER_DEBUG);
if ($type == "raw") {
$privacy_sql = "AND `item`.`allow_cid`='' AND `item`.`allow_gid`='' AND `item`.`deny_cid`='' AND `item`.`deny_gid`=''";
} else {
$privacy_sql = "";
}
// get last public wall message
$lastwall = q(
"SELECT `item`.*
FROM `item`
WHERE `item`.`contact-id` = %d AND `item`.`uid` = %d
AND ((`item`.`author-link` IN ('%s', '%s')) OR (`item`.`owner-link` IN ('%s', '%s')))
AND `item`.`type` != 'activity' $privacy_sql
ORDER BY `item`.`id` DESC
LIMIT 1",
intval($user_info['cid']),
intval(api_user()),
dbesc($user_info['url']),
dbesc(normalise_link($user_info['url'])),
dbesc($user_info['url']),
dbesc(normalise_link($user_info['url']))
);
if (DBM::is_result($lastwall)) {
$lastwall = $lastwall[0];
$in_reply_to = api_in_reply_to($lastwall);
$converted = api_convert_item($lastwall);
if ($type == "xml") {
$geo = "georss:point";
} else {
$geo = "geo";
}
$status_info = [
'created_at' => api_date($lastwall['created']),
'id' => intval($lastwall['id']),
'id_str' => (string) $lastwall['id'],
'text' => $converted["text"],
'source' => (($lastwall['app']) ? $lastwall['app'] : 'web'),
'truncated' => false,
'in_reply_to_status_id' => $in_reply_to['status_id'],
'in_reply_to_status_id_str' => $in_reply_to['status_id_str'],
'in_reply_to_user_id' => $in_reply_to['user_id'],
'in_reply_to_user_id_str' => $in_reply_to['user_id_str'],
'in_reply_to_screen_name' => $in_reply_to['screen_name'],
'user' => $user_info,
$geo => null,
'coordinates' => "",
'place' => "",
'contributors' => "",
'is_quote_status' => false,
'retweet_count' => 0,
'favorite_count' => 0,
'favorited' => $lastwall['starred'] ? true : false,
'retweeted' => false,
'possibly_sensitive' => false,
'lang' => "",
'statusnet_html' => $converted["html"],
'statusnet_conversation_id' => $lastwall['parent'],
'external_url' => System::baseUrl() . "/display/" . $lastwall['guid'],
];
if (count($converted["attachments"]) > 0) {
$status_info["attachments"] = $converted["attachments"];
}
if (count($converted["entities"]) > 0) {
$status_info["entities"] = $converted["entities"];
}
if (($lastwall['item_network'] != "") && ($status["source"] == 'web')) {
$status_info["source"] = ContactSelector::networkToName($lastwall['item_network'], $user_info['url']);
} elseif (($lastwall['item_network'] != "") && (ContactSelector::networkToName($lastwall['item_network'], $user_info['url']) != $status_info["source"])) {
$status_info["source"] = trim($status_info["source"].' ('.ContactSelector::networkToName($lastwall['item_network'], $user_info['url']).')');
}
// "uid" and "self" are only needed for some internal stuff, so remove it from here
unset($status_info["user"]["uid"]);
unset($status_info["user"]["self"]);
}
logger('status_info: '.print_r($status_info, true), LOGGER_DEBUG);
if ($type == "raw") {
return $status_info;
}
return api_format_data("statuses", $type, ['status' => $status_info]);
}
/**
* 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)
* @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 = get_app();
$user_info = api_get_user($a);
$lastwall = q(
"SELECT `item`.*
FROM `item`
INNER JOIN `contact` ON `contact`.`id`=`item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
WHERE `item`.`uid` = %d AND `verb` = '%s' AND `item`.`contact-id` = %d
AND ((`item`.`author-link` IN ('%s', '%s')) OR (`item`.`owner-link` IN ('%s', '%s')))
AND `type`!='activity'
AND `item`.`allow_cid`='' AND `item`.`allow_gid`='' AND `item`.`deny_cid`='' AND `item`.`deny_gid`=''
ORDER BY `id` DESC
LIMIT 1",
intval(api_user()),
dbesc(ACTIVITY_POST),
intval($user_info['cid']),
dbesc($user_info['url']),
dbesc(normalise_link($user_info['url'])),
dbesc($user_info['url']),
dbesc(normalise_link($user_info['url']))
);
if (DBM::is_result($lastwall)) {
$lastwall = $lastwall[0];
$in_reply_to = api_in_reply_to($lastwall);
$converted = api_convert_item($lastwall);
if ($type == "xml") {
$geo = "georss:point";
} else {
$geo = "geo";
}
$user_info['status'] = [
'text' => $converted["text"],
'truncated' => false,
'created_at' => api_date($lastwall['created']),
'in_reply_to_status_id' => $in_reply_to['status_id'],
'in_reply_to_status_id_str' => $in_reply_to['status_id_str'],
'source' => (($lastwall['app']) ? $lastwall['app'] : 'web'),
'id' => intval($lastwall['contact-id']),
'id_str' => (string) $lastwall['contact-id'],
'in_reply_to_user_id' => $in_reply_to['user_id'],
'in_reply_to_user_id_str' => $in_reply_to['user_id_str'],
'in_reply_to_screen_name' => $in_reply_to['screen_name'],
$geo => null,
'favorited' => $lastwall['starred'] ? true : false,
'statusnet_html' => $converted["html"],
'statusnet_conversation_id' => $lastwall['parent'],
'external_url' => System::baseUrl() . "/display/" . $lastwall['guid'],
];
if (count($converted["attachments"]) > 0) {
$user_info["status"]["attachments"] = $converted["attachments"];
}
if (count($converted["entities"]) > 0) {
$user_info["status"]["entities"] = $converted["entities"];
}
if (($lastwall['item_network'] != "") && ($user_info["status"]["source"] == 'web')) {
$user_info["status"]["source"] = ContactSelector::networkToName($lastwall['item_network'], $user_info['url']);
}
if (($lastwall['item_network'] != "") && (ContactSelector::networkToName($lastwall['item_network'], $user_info['url']) != $user_info["status"]["source"])) {
$user_info["status"]["source"] = trim($user_info["status"]["source"] . ' (' . ContactSelector::networkToName($lastwall['item_network'], $user_info['url']) . ')');
}
}
// "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
* @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 = get_app();
$userlist = [];
if (x($_GET, 'q')) {
$r = q("SELECT id FROM `contact` WHERE `uid` = 0 AND `name` = '%s'", dbesc($_GET["q"]));
if (!DBM::is_result($r)) {
$r = q("SELECT `id` FROM `contact` WHERE `uid` = 0 AND `nick` = '%s'", dbesc($_GET["q"]));
}
if (DBM::is_result($r)) {
$k = 0;
foreach ($r as $user) {
$user_info = api_get_user($a, $user["id"]);
if ($type == "xml") {
$userlist[$k++.":user"] = $user_info;
} else {
$userlist[] = $user_info;
}
}
$userlist = ["users" => $userlist];
} else {
throw new BadRequestException("User not found.");
}
} else {
throw new BadRequestException("User not found.");
}
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 NotFoundException if the results are empty.
*/
function api_users_lookup($type)
{
$users = [];
if (x($_REQUEST['user_id'])) {
foreach (explode(',', $_REQUEST['user_id']) as $id) {
if (!empty($id)) {
$users[] = api_get_user(get_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.
*/
function api_search($type)
{
$data = [];
$sql_extra = '';
if (!x($_REQUEST, 'q')) {
throw new BadRequestException("q parameter is required.");
}
if (x($_REQUEST, 'rpp')) {
$count = $_REQUEST['rpp'];
} elseif (x($_REQUEST, 'count')) {
$count = $_REQUEST['count'];
} else {
$count = 15;
}
$since_id = (x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0);
$max_id = (x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0);
$page = (x($_REQUEST, 'page') ? $_REQUEST['page'] - 1 : 0);
$start = $page * $count;
if ($max_id > 0) {
$sql_extra .= ' AND `item`.`id` <= ' . intval($max_id);
}
$r = dba::p(
"SELECT ".item_fieldlists()."
FROM `item` ".item_joins()."
WHERE ".item_condition()." AND (`item`.`uid` = 0 OR (`item`.`uid` = ? AND NOT `item`.`global`))
AND `item`.`body` LIKE CONCAT('%',?,'%')
$sql_extra
AND `item`.`id`>?
ORDER BY `item`.`id` DESC LIMIT ".intval($start)." ,".intval($count)." ",
api_user(),
$_REQUEST['q'],
$since_id
);
$data['status'] = api_format_items(dba::inArray($r), api_get_user(get_app()));
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)
*
* @todo Optional parameters
* @todo Add reply info
*/
function api_statuses_home_timeline($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
unset($_REQUEST["screen_name"]);
unset($_GET["screen_name"]);
$user_info = api_get_user($a);
// get last network messages
// params
$count = (x($_REQUEST, 'count') ? $_REQUEST['count'] : 20);
$page = (x($_REQUEST, 'page') ? $_REQUEST['page'] - 1 : 0);
if ($page < 0) {
$page = 0;
}
$since_id = (x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0);
$max_id = (x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0);
//$since_id = 0;//$since_id = (x($_REQUEST, 'since_id')?$_REQUEST['since_id'] : 0);
$exclude_replies = (x($_REQUEST, 'exclude_replies') ? 1 : 0);
$conversation_id = (x($_REQUEST, 'conversation_id') ? $_REQUEST['conversation_id'] : 0);
$start = $page * $count;
$sql_extra = '';
if ($max_id > 0) {
$sql_extra .= ' AND `item`.`id` <= ' . intval($max_id);
}
if ($exclude_replies > 0) {
$sql_extra .= ' AND `item`.`parent` = `item`.`id`';
}
if ($conversation_id > 0) {
$sql_extra .= ' AND `item`.`parent` = ' . intval($conversation_id);
}
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
WHERE `item`.`uid` = %d AND `verb` = '%s'
AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
$sql_extra
AND `item`.`id`>%d
ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
intval(api_user()),
dbesc(ACTIVITY_POST),
intval($since_id),
intval($start),
intval($count)
);
$ret = api_format_items($r, $user_info, false, $type);
// Set all posts from the query above to seen
$idarray = [];
foreach ($r as $item) {
$idarray[] = intval($item["id"]);
}
$idlist = implode(",", $idarray);
if ($idlist != "") {
$unseen = q("SELECT `id` FROM `item` WHERE `unseen` AND `id` IN (%s)", $idlist);
if ($unseen) {
q("UPDATE `item` SET `unseen` = 0 WHERE `unseen` AND `id` IN (%s)", $idlist);
}
}
$data = ['status' => $ret];
switch ($type) {
case "atom":
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
*/
function api_statuses_public_timeline($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
$user_info = api_get_user($a);
// get last network messages
// params
$count = (x($_REQUEST, 'count') ? $_REQUEST['count'] : 20);
$page = (x($_REQUEST, 'page') ? $_REQUEST['page'] -1 : 0);
if ($page < 0) {
$page = 0;
}
$since_id = (x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0);
$max_id = (x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0);
//$since_id = 0;//$since_id = (x($_REQUEST, 'since_id')?$_REQUEST['since_id'] : 0);
$exclude_replies = (x($_REQUEST, 'exclude_replies') ? 1 : 0);
$conversation_id = (x($_REQUEST, 'conversation_id') ? $_REQUEST['conversation_id'] : 0);
$start = $page * $count;
$sql_extra = '';
if ($exclude_replies && !$conversation_id) {
if ($max_id > 0) {
$sql_extra = 'AND `thread`.`iid` <= ' . intval($max_id);
}
$r = dba::p(
"SELECT " . item_fieldlists() . "
FROM `thread`
STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid`
" . item_joins() . "
STRAIGHT_JOIN `user` ON `user`.`uid` = `thread`.`uid`
AND NOT `user`.`hidewall`
AND `verb` = ?
AND NOT `thread`.`private`
AND `thread`.`wall`
AND `thread`.`visible`
AND NOT `thread`.`deleted`
AND NOT `thread`.`moderated`
AND `thread`.`iid` > ?
$sql_extra
ORDER BY `thread`.`iid` DESC
LIMIT " . intval($start) . ", " . intval($count),
ACTIVITY_POST,
$since_id
);
$r = dba::inArray($r);
} else {
if ($max_id > 0) {
$sql_extra = 'AND `item`.`id` <= ' . intval($max_id);
}
if ($conversation_id > 0) {
$sql_extra .= ' AND `item`.`parent` = ' . intval($conversation_id);
}
$r = dba::p(
"SELECT " . item_fieldlists() . "
FROM `item`
" . item_joins() . "
STRAIGHT_JOIN `user` ON `user`.`uid` = `item`.`uid`
AND NOT `user`.`hidewall`
AND `verb` = ?
AND NOT `item`.`private`
AND `item`.`wall`
AND `item`.`visible`
AND NOT `item`.`deleted`
AND NOT `item`.`moderated`
AND `item`.`id` > ?
$sql_extra
ORDER BY `item`.`id` DESC
LIMIT " . intval($start) . ", " . intval($count),
ACTIVITY_POST,
$since_id
);
$r = dba::inArray($r);
}
$ret = api_format_items($r, $user_info, false, $type);
$data = ['status' => $ret];
switch ($type) {
case "atom":
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.
*
* @brief Returns the list of public federated posts this node knows about
*
* @param string $type Return format: json, xml, atom, rss
* @return array|string
* @throws ForbiddenException
*/
function api_statuses_networkpublic_timeline($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
$user_info = api_get_user($a);
$since_id = x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0;
$max_id = x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0;
// pagination
$count = x($_REQUEST, 'count') ? $_REQUEST['count'] : 20;
$page = x($_REQUEST, 'page') ? $_REQUEST['page'] : 1;
if ($page < 1) {
$page = 1;
}
$start = ($page - 1) * $count;
$sql_extra = '';
if ($max_id > 0) {
$sql_extra = 'AND `thread`.`iid` <= ' . intval($max_id);
}
$r = dba::p(
"SELECT " . item_fieldlists() . "
FROM `thread`
STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid`
" . item_joins() . "
WHERE `thread`.`uid` = 0
AND `verb` = ?
AND NOT `thread`.`private`
AND `thread`.`visible`
AND NOT `thread`.`deleted`
AND NOT `thread`.`moderated`
AND `thread`.`iid` > ?
$sql_extra
ORDER BY `thread`.`iid` DESC
LIMIT " . intval($start) . ", " . intval($count),
ACTIVITY_POST,
$since_id
);
$r = dba::inArray($r);
$ret = api_format_items($r, $user_info, false, $type);
$data = ['status' => $ret];
switch ($type) {
case "atom":
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)
*
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id
*/
function api_statuses_show($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
$user_info = api_get_user($a);
// params
$id = intval($a->argv[3]);
if ($id == 0) {
$id = intval($_REQUEST["id"]);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4]);
}
logger('API: api_statuses_show: ' . $id);
$conversation = (x($_REQUEST, 'conversation') ? 1 : 0);
$sql_extra = '';
if ($conversation) {
$sql_extra .= " AND `item`.`parent` = %d ORDER BY `id` ASC ";
} else {
$sql_extra .= " AND `item`.`id` = %d";
}
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item`
INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
WHERE `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
AND `item`.`uid` = %d AND `item`.`verb` = '%s'
$sql_extra",
intval(api_user()),
dbesc(ACTIVITY_POST),
intval($id)
);
/// @TODO How about copying this to above methods which don't check $r ?
if (!DBM::is_result($r)) {
throw new BadRequestException("There is no status with this id.");
}
$ret = api_format_items($r, $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)
*
* @todo nothing to say?
*/
function api_conversation_show($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
$user_info = api_get_user($a);
// params
$id = intval($a->argv[3]);
$count = (x($_REQUEST, 'count') ? $_REQUEST['count'] : 20);
$page = (x($_REQUEST, 'page') ? $_REQUEST['page'] - 1 : 0);
if ($page < 0) {
$page = 0;
}
$since_id = (x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0);
$max_id = (x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0);
$start = $page*$count;
if ($id == 0) {
$id = intval($_REQUEST["id"]);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4]);
}
logger('API: api_conversation_show: '.$id);
$r = q("SELECT `parent` FROM `item` WHERE `id` = %d", intval($id));
if (DBM::is_result($r)) {
$id = $r[0]["parent"];
}
$sql_extra = '';
if ($max_id > 0) {
$sql_extra = ' AND `item`.`id` <= ' . intval($max_id);
}
// Not sure why this query was so complicated. We should keep it here for a while,
// just to make sure that we really don't need it.
// FROM `item` INNER JOIN (SELECT `uri`,`parent` FROM `item` WHERE `id` = %d) AS `temp1`
// ON (`item`.`thr-parent` = `temp1`.`uri` AND `item`.`parent` = `temp1`.`parent`)
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
WHERE `item`.`parent` = %d AND `item`.`visible`
AND NOT `item`.`moderated` AND NOT `item`.`deleted`
AND `item`.`uid` = %d AND `item`.`verb` = '%s'
AND `item`.`id`>%d $sql_extra
ORDER BY `item`.`id` DESC LIMIT %d ,%d",
intval($id),
intval(api_user()),
dbesc(ACTIVITY_POST),
intval($since_id),
intval($start),
intval($count)
);
if (!DBM::is_result($r)) {
throw new BadRequestException("There is no status with this id.");
}
$ret = api_format_items($r, $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)
*
* @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 = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
api_get_user($a);
// params
$id = intval($a->argv[3]);
if ($id == 0) {
$id = intval($_REQUEST["id"]);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4]);
}
logger('API: api_statuses_repeat: '.$id);
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`, `contact`.`nick` as `reply_author`,
`contact`.`name`, `contact`.`photo` as `reply_photo`, `contact`.`url` as `reply_url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item`
INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
WHERE `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
AND NOT `item`.`private` AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = ''
AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
AND `item`.`id`=%d",
intval($id)
);
/// @TODO other style than above functions!
if (DBM::is_result($r) && $r[0]['body'] != "") {
if (strpos($r[0]['body'], "[/share]") !== false) {
$pos = strpos($r[0]['body'], "[share");
$post = substr($r[0]['body'], $pos);
} else {
$post = share_header($r[0]['author-name'], $r[0]['author-link'], $r[0]['author-avatar'], $r[0]['guid'], $r[0]['created'], $r[0]['plink']);
$post .= $r[0]['body'];
$post .= "[/share]";
}
$_REQUEST['body'] = $post;
$_REQUEST['profile_uid'] = api_user();
$_REQUEST['type'] = 'wall';
$_REQUEST['api_source'] = true;
if (!x($_REQUEST, "source")) {
$_REQUEST["source"] = api_source();
}
item_post($a);
} else {
throw new ForbiddenException();
}
// this should output the last post (the one we just posted).
$called_api = null;
return api_status_show($type);
}
/// @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)
*
* @see https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id
*/
function api_statuses_destroy($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
api_get_user($a);
// params
$id = intval($a->argv[3]);
if ($id == 0) {
$id = intval($_REQUEST["id"]);
}
// Hotot workaround
if ($id == 0) {
$id = intval($a->argv[4]);
}
logger('API: api_statuses_destroy: '.$id);
$ret = api_statuses_show($type);
Item::deleteById($id);
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)
*
* @see http://developer.twitter.com/doc/get/statuses/mentions
*/
function api_statuses_mentions($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
unset($_REQUEST["screen_name"]);
unset($_GET["screen_name"]);
$user_info = api_get_user($a);
// get last network messages
// params
$since_id = defaults($_REQUEST, 'since_id', 0);
$max_id = defaults($_REQUEST, 'max_id' , 0);
$count = defaults($_REQUEST, 'count' , 20);
$page = defaults($_REQUEST, 'page' , 1);
if ($page < 1) {
$page = 1;
}
$start = ($page - 1) * $count;
// Ugly code - should be changed
$myurl = System::baseUrl() . '/profile/'. $a->user['nickname'];
$myurl = substr($myurl, strpos($myurl, '://') + 3);
$myurl = str_replace('www.', '', $myurl);
$sql_extra = '';
if ($max_id > 0) {
$sql_extra .= ' AND `item`.`id` <= ' . intval($max_id);
}
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item` FORCE INDEX (`uid_id`)
STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
WHERE `item`.`uid` = %d AND `verb` = '%s'
AND NOT (`item`.`author-link` IN ('https://%s', 'http://%s'))
AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
AND `item`.`parent` IN (SELECT `iid` FROM `thread` WHERE `uid` = %d AND `mention` AND !`ignored`)
$sql_extra
AND `item`.`id`>%d
ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
intval(api_user()),
dbesc(ACTIVITY_POST),
dbesc(protect_sprintf($myurl)),
dbesc(protect_sprintf($myurl)),
intval(api_user()),
intval($since_id),
intval($start),
intval($count)
);
$ret = api_format_items($r, $user_info, false, $type);
$data = ['status' => $ret];
switch ($type) {
case "atom":
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.
*
* @brief Returns a user's public timeline
*
* @param string $type Either "json" or "xml"
* @return string|array
* @throws ForbiddenException
* @see https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline
*/
function api_statuses_user_timeline($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
$user_info = api_get_user($a);
logger(
"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 = x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0;
$max_id = x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0;
$exclude_replies = x($_REQUEST, 'exclude_replies') ? 1 : 0;
$conversation_id = x($_REQUEST, 'conversation_id') ? $_REQUEST['conversation_id'] : 0;
// pagination
$count = x($_REQUEST, 'count') ? $_REQUEST['count'] : 20;
$page = x($_REQUEST, 'page') ? $_REQUEST['page'] : 1;
if ($page < 1) {
$page = 1;
}
$start = ($page - 1) * $count;
$sql_extra = '';
if ($user_info['self'] == 1) {
$sql_extra .= " AND `item`.`wall` = 1 ";
}
if ($exclude_replies > 0) {
$sql_extra .= ' AND `item`.`parent` = `item`.`id`';
}
if ($conversation_id > 0) {
$sql_extra .= ' AND `item`.`parent` = ' . intval($conversation_id);
}
if ($max_id > 0) {
$sql_extra .= ' AND `item`.`id` <= ' . intval($max_id);
}
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item` FORCE INDEX (`uid_contactid_id`)
STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
WHERE `item`.`uid` = %d AND `verb` = '%s'
AND `item`.`contact-id` = %d
AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
$sql_extra
AND `item`.`id` > %d
ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
intval(api_user()),
dbesc(ACTIVITY_POST),
intval($user_info['cid']),
intval($since_id),
intval($start),
intval($count)
);
$ret = api_format_items($r, $user_info, true, $type);
$data = ['status' => $ret];
switch ($type) {
case "atom":
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)
*
* @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 = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
// for versioned api.
/// @TODO We need a better global soluton
$action_argv_id = 2;
if ($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]);
} else {
/// @TODO use x() to check if _REQUEST contains 'id'
$itemid = intval($_REQUEST['id']);
}
$item = q("SELECT * FROM `item` WHERE `id`=%d AND `uid`=%d LIMIT 1", $itemid, api_user());
if (!DBM::is_result($item) || count($item) == 0) {
throw new BadRequestException("Invalid item.");
}
switch ($action) {
case "create":
$item[0]['starred'] = 1;
break;
case "destroy":
$item[0]['starred'] = 0;
break;
default:
throw new BadRequestException("Invalid action ".$action);
}
$r = Item::update(['starred' => $item[0]['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":
case "rss":
$data = api_rss_extra($a, $data, $user_info);
}
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
*/
function api_favorites($type)
{
global $called_api;
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
$called_api = [];
$user_info = api_get_user($a);
// in friendica starred item are private
// return favorites only for self
logger('api_favorites: self:' . $user_info['self']);
if ($user_info['self'] == 0) {
$ret = [];
} else {
$sql_extra = "";
// params
$since_id = (x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0);
$max_id = (x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0);
$count = (x($_GET, 'count') ? $_GET['count'] : 20);
$page = (x($_REQUEST, 'page') ? $_REQUEST['page'] -1 : 0);
if ($page < 0) {
$page = 0;
}
$start = $page*$count;
if ($max_id > 0) {
$sql_extra .= ' AND `item`.`id` <= ' . intval($max_id);
}
$r = q(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`
FROM `item`, `contact`
WHERE `item`.`uid` = %d
AND `item`.`visible` = 1 AND `item`.`moderated` = 0 AND `item`.`deleted` = 0
AND `item`.`starred` = 1
AND `contact`.`id` = `item`.`contact-id`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
$sql_extra
AND `item`.`id`>%d
ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
intval(api_user()),
intval($since_id),
intval($start),
intval($count)
);
$ret = api_format_items($r, $user_info, false, $type);
}
$data = ['status' => $ret];
switch ($type) {
case "atom":
case "rss":
$data = api_rss_extra($a, $data, $user_info);
}
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
*/
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']),
'sender_screen_name' => $sender['screen_name'],
'recipient_screen_name' => $recipient['screen_name'],
'sender' => $sender,
'recipient' => $recipient,
'title' => "",
'friendica_seen' => $item['seen'],
'friendica_parent_uri' => $item['parent-uri'],
];
// "uid" and "self" are only needed for some internal stuff, so remove it from here
unset($ret["sender"]["uid"]);
unset($ret["sender"]["self"]);
unset($ret["recipient"]["uid"]);
unset($ret["recipient"]["self"]);
//don't send title to regular StatusNET requests to avoid confusing these apps
if (x($_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 (x($_GET, 'getUserObjects') && $_GET['getUserObjects'] == 'false') {
unset($ret['sender']);
unset($ret['recipient']);
}
return $ret;
}
/**
*
* @param array $item
*
* @return array
*/
function api_convert_item($item)
{
$body = $item['body'];
$attachments = api_get_attachments($body);
// Workaround for ostatus messages where the title is identically to the body
$html = BBCode::convert(api_clean_plain_items($body), false, 2, true);
$statusbody = trim(HTML::toPlaintext($html, 0));
// handle data: images
$statusbody = api_format_items_embeded_images($item, $statusbody);
$statustitle = trim($item['title']);
if (($statustitle != '') && (strpos($statusbody, $statustitle) !== false)) {
$statustext = trim($statusbody);
} else {
$statustext = trim($statustitle."\n\n".$statusbody);
}
if (($item["network"] == NETWORK_FEED) && (strlen($statustext)> 1000)) {
$statustext = substr($statustext, 0, 1000)."... \n".$item["plink"];
}
$statushtml = BBCode::convert(api_clean_attachments($body), false);
// Workaround for clients with limited HTML parser functionality
$search = ["<br>", "<blockquote>", "</blockquote>",
"<h1>", "</h1>", "<h2>", "</h2>",
"<h3>", "</h3>", "<h4>", "</h4>",
"<h5>", "</h5>", "<h6>", "</h6>"];
$replace = ["<br>", "<br><blockquote>", "</blockquote><br>",
"<br><h1>", "</h1><br>", "<br><h2>", "</h2><br>",
"<br><h3>", "</h3><br>", "<br><h4>", "</h4><br>",
"<br><h5>", "</h5><br>", "<br><h6>", "</h6><br>"];
$statushtml = str_replace($search, $replace, $statushtml);
if ($item['title'] != "") {
$statushtml = "<br><h4>" . BBCode::convert($item['title']) . "</h4><br>" . $statushtml;
}
do {
$oldtext = $statushtml;
$statushtml = str_replace("<br><br>", "<br>", $statushtml);
} while ($oldtext != $statushtml);
if (substr($statushtml, 0, 4) == '<br>') {
$statushtml = substr($statushtml, 4);
}
if (substr($statushtml, 0, -4) == '<br>') {
$statushtml = substr($statushtml, -4);
}
// feeds without body should contain the link
if (($item['network'] == NETWORK_FEED) && (strlen($item['body']) == 0)) {
$statushtml .= BBCode::convert($item['plink']);
}
$entities = api_get_entitities($statustext, $body);
return [
"text" => $statustext,
"html" => $statushtml,
"attachments" => $attachments,
"entities" => $entities
];
}
/**
*
* @param string $body
*
* @return array|false
*/
function api_get_attachments(&$body)
{
$text = $body;
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $text);
$URLSearchString = "^\[\]";
$ret = preg_match_all("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $text, $images);
if (!$ret) {
return false;
}
$attachments = [];
foreach ($images[1] as $image) {
$imagedata = Image::getInfoFromURL($image);
if ($imagedata) {
$attachments[] = ["url" => $image, "mimetype" => $imagedata["mime"], "size" => $imagedata["size"]];
}
}
if (strstr($_SERVER['HTTP_USER_AGENT'], "AndStatus")) {
foreach ($images[0] as $orig) {
$body = str_replace($orig, "", $body);
}
}
return $attachments;
}
/**
*
* @param string $text
* @param string $bbcode
*
* @return array
* @todo Links at the first character of the post
*/
function api_get_entitities(&$text, $bbcode)
{
$include_entities = strtolower(x($_REQUEST, 'include_entities') ? $_REQUEST['include_entities'] : "false");
if ($include_entities != "true") {
preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images);
foreach ($images[1] as $image) {
$replace = proxy_url($image);
$text = str_replace($image, $replace, $text);
}
return [];
}
$bbcode = BBCode::cleanPictureLinks($bbcode);
// Change pure links in text to bbcode uris
$bbcode = preg_replace("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url=$2]$2[/url]', $bbcode);
$entities = [];
$entities["hashtags"] = [];
$entities["symbols"] = [];
$entities["urls"] = [];
$entities["user_mentions"] = [];
$URLSearchString = "^\[\]";
$bbcode = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '#$2', $bbcode);
$bbcode = preg_replace("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", '[url=$1]$2[/url]', $bbcode);
//$bbcode = preg_replace("/\[url\](.*?)\[\/url\]/ism",'[url=$1]$1[/url]',$bbcode);
$bbcode = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url=$1]$1[/url]', $bbcode);
$bbcode = preg_replace(
"/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism",
'[url=https://www.youtube.com/watch?v=$1]https://www.youtube.com/watch?v=$1[/url]',
$bbcode
);
$bbcode = preg_replace("/\[youtube\](.*?)\[\/youtube\]/ism", '[url=$1]$1[/url]', $bbcode);
$bbcode = preg_replace(
"/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism",
'[url=https://vimeo.com/$1]https://vimeo.com/$1[/url]',
$bbcode
);
$bbcode = preg_replace("/\[vimeo\](.*?)\[\/vimeo\]/ism", '[url=$1]$1[/url]', $bbcode);
$bbcode = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $bbcode);
//preg_match_all("/\[url\]([$URLSearchString]*)\[\/url\]/ism", $bbcode, $urls1);
preg_match_all("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $bbcode, $urls);
$ordered_urls = [];
foreach ($urls[1] as $id => $url) {
//$start = strpos($text, $url, $offset);
$start = iconv_strpos($text, $url, 0, "UTF-8");
if (!($start === false)) {
$ordered_urls[$start] = ["url" => $url, "title" => $urls[2][$id]];
}
}
ksort($ordered_urls);
$offset = 0;
//foreach ($urls[1] AS $id=>$url) {
foreach ($ordered_urls as $url) {
if ((substr($url["title"], 0, 7) != "http://") && (substr($url["title"], 0, 8) != "https://")
&& !strpos($url["title"], "http://") && !strpos($url["title"], "https://")
) {
$display_url = $url["title"];
} else {
$display_url = str_replace(["http://www.", "https://www."], ["", ""], $url["url"]);
$display_url = str_replace(["http://", "https://"], ["", ""], $display_url);
if (strlen($display_url) > 26) {
$display_url = substr($display_url, 0, 25)."…";
}
}
//$start = strpos($text, $url, $offset);
$start = iconv_strpos($text, $url["url"], $offset, "UTF-8");
if (!($start === false)) {
$entities["urls"][] = ["url" => $url["url"],
"expanded_url" => $url["url"],
"display_url" => $display_url,
"indices" => [$start, $start+strlen($url["url"])]];
$offset = $start + 1;
}
}
preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images);
$ordered_images = [];
foreach ($images[1] as $image) {
//$start = strpos($text, $url, $offset);
$start = iconv_strpos($text, $image, 0, "UTF-8");
if (!($start === false)) {
$ordered_images[$start] = $image;
}
}
//$entities["media"] = array();
$offset = 0;
foreach ($ordered_images as $url) {
$display_url = str_replace(["http://www.", "https://www."], ["", ""], $url);
$display_url = str_replace(["http://", "https://"], ["", ""], $display_url);
if (strlen($display_url) > 26) {
$display_url = substr($display_url, 0, 25)."…";
}
$start = iconv_strpos($text, $url, $offset, "UTF-8");
if (!($start === false)) {
$image = Image::getInfoFromURL($url);
if ($image) {
// If image cache is activated, then use the following sizes:
// thumb (150), small (340), medium (600) and large (1024)
if (!Config::get("system", "proxy_disabled")) {
$media_url = proxy_url($url);
$sizes = [];
$scale = Image::getScalingDimensions($image[0], $image[1], 150);
$sizes["thumb"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
if (($image[0] > 150) || ($image[1] > 150)) {
$scale = Image::getScalingDimensions($image[0], $image[1], 340);
$sizes["small"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
}
$scale = Image::getScalingDimensions($image[0], $image[1], 600);
$sizes["medium"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
if (($image[0] > 600) || ($image[1] > 600)) {
$scale = Image::getScalingDimensions($image[0], $image[1], 1024);
$sizes["large"] = ["w" => $scale["width"], "h" => $scale["height"], "resize" => "fit"];
}
} else {
$media_url = $url;
$sizes["medium"] = ["w" => $image[0], "h" => $image[1], "resize" => "fit"];
}
$entities["media"][] = [
"id" => $start+1,
"id_str" => (string)$start+1,
"indices" => [$start, $start+strlen($url)],
"media_url" => normalise_link($media_url),
"media_url_https" => $media_url,
"url" => $url,
"display_url" => $display_url,
"expanded_url" => $url,
"type" => "photo",
"sizes" => $sizes];
}
$offset = $start + 1;
}
}
return $entities;
}
/**
*
* @param array $item
* @param string $text
*
* @return string
*/
function api_format_items_embeded_images($item, $text)
{
$text = preg_replace_callback(
'|data:image/([^;]+)[^=]+=*|m',
function () use ($item) {
return System::baseUrl() . '/display/' . $item['guid'];
},
$text
);
return $text;
}
/**
* @brief return <a href='url'>name</a> as array
*
* @param string $txt text
* @return array
* 'name' => 'name',
* 'url => 'url'
*/
function api_contactlink_to_array($txt)
{
$match = [];
$r = preg_match_all('|<a href="([^"]*)">([^<]*)</a>|', $txt, $match);
if ($r && count($match)==3) {
$res = [
'name' => $match[2],
'url' => $match[1]
];
} else {
$res = [
'name' => $txt,
'url' => ""
];
}
return $res;
}
/**
* @brief return likes, dislikes and attend status for item
*
* @param array $item array
* @param string $type Return type (atom, rss, xml, json)
*
* @return array
* likes => int count,
* dislikes => int count
*/
function api_format_items_activities(&$item, $type = "json")
{
$a = get_app();
$activities = [
'like' => [],
'dislike' => [],
'attendyes' => [],
'attendno' => [],
'attendmaybe' => [],
];
$items = q(
'SELECT * FROM item
WHERE uid=%d AND `thr-parent`="%s" AND visible AND NOT deleted',
intval($item['uid']),
dbesc($item['uri'])
);
foreach ($items as $i) {
// not used as result should be structured like other user data
//builtin_activity_puller($i, $activities);
// get user data and add it to the array of the activity
$user = api_get_user($a, $i['author-link']);
switch ($i['verb']) {
case ACTIVITY_LIKE:
$activities['like'][] = $user;
break;
case ACTIVITY_DISLIKE:
$activities['dislike'][] = $user;
break;
case ACTIVITY_ATTEND:
$activities['attendyes'][] = $user;
break;
case ACTIVITY_ATTENDNO:
$activities['attendno'][] = $user;
break;
case ACTIVITY_ATTENDMAYBE:
$activities['attendmaybe'][] = $user;
break;
default:
break;
}
}
if ($type == "xml") {
$xml_activities = [];
foreach ($activities as $k => $v) {
// change xml element from "like" to "friendica:like"
$xml_activities["friendica:".$k] = $v;
// add user data into xml output
$k_user = 0;
foreach ($v as $user) {
$xml_activities["friendica:".$k][$k_user++.":user"] = $user;
}
}
$activities = $xml_activities;
}
return $activities;
}
/**
* @brief return data from profiles
*
* @param array $profile_row array containing data from db table 'profile'
* @return array
*/
function api_format_items_profiles($profile_row)
{
$profile = [
'profile_id' => $profile_row['id'],
'profile_name' => $profile_row['profile-name'],
'is_default' => $profile_row['is-default'] ? true : false,
'hide_friends' => $profile_row['hide-friends'] ? true : false,
'profile_photo' => $profile_row['photo'],
'profile_thumb' => $profile_row['thumb'],
'publish' => $profile_row['publish'] ? true : false,
'net_publish' => $profile_row['net-publish'] ? true : false,
'description' => $profile_row['pdesc'],
'date_of_birth' => $profile_row['dob'],
'address' => $profile_row['address'],
'city' => $profile_row['locality'],
'region' => $profile_row['region'],
'postal_code' => $profile_row['postal-code'],
'country' => $profile_row['country-name'],
'hometown' => $profile_row['hometown'],
'gender' => $profile_row['gender'],
'marital' => $profile_row['marital'],
'marital_with' => $profile_row['with'],
'marital_since' => $profile_row['howlong'],
'sexual' => $profile_row['sexual'],
'politic' => $profile_row['politic'],
'religion' => $profile_row['religion'],
'public_keywords' => $profile_row['pub_keywords'],
'private_keywords' => $profile_row['prv_keywords'],
'likes' => BBCode::convert(api_clean_plain_items($profile_row['likes']) , false, 2),
'dislikes' => BBCode::convert(api_clean_plain_items($profile_row['dislikes']) , false, 2),
'about' => BBCode::convert(api_clean_plain_items($profile_row['about']) , false, 2),
'music' => BBCode::convert(api_clean_plain_items($profile_row['music']) , false, 2),
'book' => BBCode::convert(api_clean_plain_items($profile_row['book']) , false, 2),
'tv' => BBCode::convert(api_clean_plain_items($profile_row['tv']) , false, 2),
'film' => BBCode::convert(api_clean_plain_items($profile_row['film']) , false, 2),
'interest' => BBCode::convert(api_clean_plain_items($profile_row['interest']) , false, 2),
'romance' => BBCode::convert(api_clean_plain_items($profile_row['romance']) , false, 2),
'work' => BBCode::convert(api_clean_plain_items($profile_row['work']) , false, 2),
'education' => BBCode::convert(api_clean_plain_items($profile_row['education']), false, 2),
'social_networks' => BBCode::convert(api_clean_plain_items($profile_row['contact']) , false, 2),
'homepage' => $profile_row['homepage'],
'users' => null
];
return $profile;
}
/**
* @brief format items to be returned by api
*
* @param array $r array of items
* @param array $user_info
* @param bool $filter_user filter items by $user_info
* @param string $type Return type (atom, rss, xml, json)
*/
function api_format_items($r, $user_info, $filter_user = false, $type = "json")
{
$a = get_app();
$ret = [];
foreach ($r as $item) {
localize_item($item);
list($status_user, $owner_user) = api_item_get_user($a, $item);
// Look if the posts are matching if they should be filtered by user id
if ($filter_user && ($status_user["id"] != $user_info["id"])) {
continue;
}
$in_reply_to = api_in_reply_to($item);
$converted = api_convert_item($item);
if ($type == "xml") {
$geo = "georss:point";
} else {
$geo = "geo";
}
$status = [
'text' => $converted["text"],
'truncated' => false,
'created_at'=> api_date($item['created']),
'in_reply_to_status_id' => $in_reply_to['status_id'],
'in_reply_to_status_id_str' => $in_reply_to['status_id_str'],
'source' => (($item['app']) ? $item['app'] : 'web'),
'id' => intval($item['id']),
'id_str' => (string) intval($item['id']),
'in_reply_to_user_id' => $in_reply_to['user_id'],
'in_reply_to_user_id_str' => $in_reply_to['user_id_str'],
'in_reply_to_screen_name' => $in_reply_to['screen_name'],
$geo => null,
'favorited' => $item['starred'] ? true : false,
'user' => $status_user ,
'friendica_owner' => $owner_user,
//'entities' => NULL,
'statusnet_html' => $converted["html"],
'statusnet_conversation_id' => $item['parent'],
'external_url' => System::baseUrl() . "/display/" . $item['guid'],
'friendica_activities' => api_format_items_activities($item, $type),
];
if (count($converted["attachments"]) > 0) {
$status["attachments"] = $converted["attachments"];
}
if (count($converted["entities"]) > 0) {
$status["entities"] = $converted["entities"];
}
if (($item['item_network'] != "") && ($status["source"] == 'web')) {
$status["source"] = ContactSelector::networkToName($item['item_network'], $user_info['url']);
} elseif (($item['item_network'] != "") && (ContactSelector::networkToName($item['item_network'], $user_info['url']) != $status["source"])) {
$status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['item_network'], $user_info['url']).')');
}
// Retweets are only valid for top postings
// It doesn't work reliable with the link if its a feed
//$IsRetweet = ($item['owner-link'] != $item['author-link']);
//if ($IsRetweet)
// $IsRetweet = (($item['owner-name'] != $item['author-name']) || ($item['owner-avatar'] != $item['author-avatar']));
if ($item["id"] == $item["parent"]) {
$retweeted_item = api_share_as_retweet($item);
if ($retweeted_item !== false) {
$retweeted_status = $status;
try {
$retweeted_status["user"] = api_get_user($a, $retweeted_item["author-link"]);
} catch (BadRequestException $e) {
// user not found. should be found?
/// @todo check if the user should be always found
$retweeted_status["user"] = [];
}
$rt_converted = api_convert_item($retweeted_item);
$retweeted_status['text'] = $rt_converted["text"];
$retweeted_status['statusnet_html'] = $rt_converted["html"];
$retweeted_status['friendica_activities'] = api_format_items_activities($retweeted_item, $type);
$retweeted_status['created_at'] = api_date($retweeted_item['created']);
$status['retweeted_status'] = $retweeted_status;
}
}
// "uid" and "self" are only needed for some internal stuff, so remove it from here
unset($status["user"]["uid"]);
unset($status["user"]["self"]);
if ($item["coord"] != "") {
$coords = explode(' ', $item["coord"]);
if (count($coords) == 2) {
if ($type == "json") {
$status["geo"] = ['type' => 'Point',
'coordinates' => [(float) $coords[0],
(float) $coords[1]]];
} else {// Not sure if this is the official format - if someone founds a documentation we can check
$status["georss:point"] = $item["coord"];
}
}
}
$ret[] = $status;
};
return $ret;
}
/**
* Returns the remaining number of API requests available to the user before the API limit is reached.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
*/
function api_account_rate_limit_status($type)
{
if ($type == "xml") {
$hash = [
'remaining-hits' => '150',
'@attributes' => ["type" => "integer"],
'hourly-limit' => '150',
'@attributes2' => ["type" => "integer"],
'reset-time' => DateTimeFormat::utc('now + 1 hour', DateTimeFormat::ATOM),
'@attributes3' => ["type" => "datetime"],
'reset_time_in_seconds' => strtotime('now + 1 hour'),
'@attributes4' => ["type" => "integer"],
];
} else {
$hash = [
'reset_time_in_seconds' => strtotime('now + 1 hour'),
'remaining_hits' => '150',
'hourly_limit' => '150',
'reset_time' => api_date(DateTimeFormat::utc('now + 1 hour', DateTimeFormat::ATOM)),
];
}
return api_format_data('hash', $type, ['hash' => $hash]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/account/rate_limit_status', 'api_account_rate_limit_status', true);
/**
* Returns the string "ok" in the requested format with a 200 OK HTTP status code.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
*/
function api_help_test($type)
{
if ($type == 'xml') {
$ok = "true";
} else {
$ok = "ok";
}
return api_format_data('ok', $type, ["ok" => $ok]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/help/test', 'api_help_test', false);
/**
* Returns all lists the user subscribes to.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list
*/
function api_lists_list($type)
{
$ret = [];
/// @TODO $ret is not filled here?
return api_format_data('lists', $type, ["lists_list" => $ret]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/lists/list', 'api_lists_list', true);
api_register_func('api/lists/subscriptions', 'api_lists_list', true);
/**
* Returns all groups the user owns.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
*/
function api_lists_ownerships($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
// params
$user_info = api_get_user($a);
$uid = $user_info['uid'];
$groups = dba::select('group', [], ['deleted' => 0, 'uid' => $uid]);
// loop through all groups
$lists = [];
foreach ($groups as $group) {
if ($group['visible']) {
$mode = 'public';
} else {
$mode = 'private';
}
$lists[] = [
'name' => $group['name'],
'id' => intval($group['id']),
'id_str' => (string) $group['id'],
'user' => $user_info,
'mode' => $mode
];
}
return api_format_data("lists", $type, ['lists' => ['lists' => $lists]]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/lists/ownerships', 'api_lists_ownerships', true);
/**
* Returns recent statuses from users in the specified group.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @see https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships
*/
function api_lists_statuses($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
unset($_REQUEST["user_id"]);
unset($_GET["user_id"]);
unset($_REQUEST["screen_name"]);
unset($_GET["screen_name"]);
$user_info = api_get_user($a);
if (!x($_REQUEST, 'list_id')) {
throw new BadRequestException('list_id not specified');
}
// params
$count = (x($_REQUEST, 'count') ? $_REQUEST['count'] : 20);
$page = (x($_REQUEST, 'page') ? $_REQUEST['page'] - 1 : 0);
if ($page < 0) {
$page = 0;
}
$since_id = (x($_REQUEST, 'since_id') ? $_REQUEST['since_id'] : 0);
$max_id = (x($_REQUEST, 'max_id') ? $_REQUEST['max_id'] : 0);
//$since_id = 0;//$since_id = (x($_REQUEST, 'since_id')?$_REQUEST['since_id'] : 0);
$exclude_replies = (x($_REQUEST, 'exclude_replies') ? 1 : 0);
$conversation_id = (x($_REQUEST, 'conversation_id') ? $_REQUEST['conversation_id'] : 0);
$start = $page * $count;
$sql_extra = '';
if ($max_id > 0) {
$sql_extra .= ' AND `item`.`id` <= ' . intval($max_id);
}
if ($exclude_replies > 0) {
$sql_extra .= ' AND `item`.`parent` = `item`.`id`';
}
if ($conversation_id > 0) {
$sql_extra .= ' AND `item`.`parent` = ' . intval($conversation_id);
}
$statuses = dba::p(
"SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
`contact`.`id` AS `cid`, `group_member`.`gid`
FROM `item`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
AND (NOT `contact`.`blocked` OR `contact`.`pending`)
STRAIGHT_JOIN `group_member` ON `group_member`.`contact-id` = `item`.`contact-id`
WHERE `item`.`uid` = ? AND `verb` = ?
AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
AND `item`.`id`>?
AND `group_member`.`gid` = ?
ORDER BY `item`.`id` DESC LIMIT ".intval($start)." ,".intval($count),
api_user(),
ACTIVITY_POST,
$since_id,
$_REQUEST['list_id']
);
$items = api_format_items($statuses, $user_info, false, $type);
$data = ['status' => $items];
switch ($type) {
case "atom":
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/lists/statuses', 'api_lists_statuses', true);
/**
* Considers friends and followers lists to be private and won't return
* anything if any user_id parameter is passed.
*
* @brief Returns either the friends of the follower list
*
* @param string $qtype Either "friends" or "followers"
* @return boolean|array
* @throws ForbiddenException
*/
function api_statuses_f($qtype)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
// pagination
$count = x($_GET, 'count') ? $_GET['count'] : 20;
$page = x($_GET, 'page') ? $_GET['page'] : 1;
if ($page < 1) {
$page = 1;
}
$start = ($page - 1) * $count;
$user_info = api_get_user($a);
if (x($_GET, 'cursor') && $_GET['cursor'] == 'undefined') {
/* this is to stop Hotot to load friends multiple times
* I'm not sure if I'm missing return something or
* is a bug in hotot. Workaround, meantime
*/
/*$ret=Array();
return array('$users' => $ret);*/
return false;
}
$sql_extra = '';
if ($qtype == 'friends') {
$sql_extra = sprintf(" AND ( `rel` = %d OR `rel` = %d ) ", intval(CONTACT_IS_SHARING), intval(CONTACT_IS_FRIEND));
} elseif ($qtype == 'followers') {
$sql_extra = sprintf(" AND ( `rel` = %d OR `rel` = %d ) ", intval(CONTACT_IS_FOLLOWER), intval(CONTACT_IS_FRIEND));
}
// friends and followers only for self
if ($user_info['self'] == 0) {
$sql_extra = " AND false ";
}
if ($qtype == 'blocks') {
$sql_filter = 'AND `blocked` AND NOT `pending`';
} elseif ($qtype == 'incoming') {
$sql_filter = 'AND `pending`';
} else {
$sql_filter = 'AND (NOT `blocked` OR `pending`)';
}
$r = q(
"SELECT `nurl`
FROM `contact`
WHERE `uid` = %d
AND NOT `self`
$sql_filter
$sql_extra
ORDER BY `nick`
LIMIT %d, %d",
intval(api_user()),
intval($start),
intval($count)
);
$ret = [];
foreach ($r as $cid) {
$user = api_get_user($a, $cid['nurl']);
// "uid" and "self" are only needed for some internal stuff, so remove it from here
unset($user["uid"]);
unset($user["self"]);
if ($user) {
$ret[] = $user;
}
}
return ['user' => $ret];
}
/**
* Returns the user's friends.
*
* @brief Returns the list of friends of the provided user
*
* @deprecated By Twitter API in favor of friends/list
*
* @param string $type Either "json" or "xml"
* @return boolean|string|array
*/
function api_statuses_friends($type)
{
$data = api_statuses_f("friends");
if ($data === false) {
return false;
}
return api_format_data("users", $type, $data);
}
/**
* Returns the user's followers.
*
* @brief Returns the list of followers of the provided user
*
* @deprecated By Twitter API in favor of friends/list
*
* @param string $type Either "json" or "xml"
* @return boolean|string|array
*/
function api_statuses_followers($type)
{
$data = api_statuses_f("followers");
if ($data === false) {
return false;
}
return api_format_data("users", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/statuses/friends', 'api_statuses_friends', true);
api_register_func('api/statuses/followers', 'api_statuses_followers', true);
/**
* Returns the list of blocked users
*
* @see https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-list
*
* @param string $type Either "json" or "xml"
*
* @return boolean|string|array
*/
function api_blocks_list($type)
{
$data = api_statuses_f('blocks');
if ($data === false) {
return false;
}
return api_format_data("users", $type, $data);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/blocks/list', 'api_blocks_list', true);
/**
* Returns the list of pending users IDs
*
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-incoming
*
* @param string $type Either "json" or "xml"
*
* @return boolean|string|array
*/
function api_friendships_incoming($type)
{
$data = api_statuses_f('incoming');
if ($data === false) {
return false;
}
$ids = [];
foreach ($data['user'] as $user) {
$ids[] = $user['id'];
}
return api_format_data("ids", $type, ['id' => $ids]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/friendships/incoming', 'api_friendships_incoming', true);
/**
* Returns the instance's configuration information.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
*/
function api_statusnet_config($type)
{
$a = get_app();
$name = $a->config['sitename'];
$server = $a->get_hostname();
$logo = System::baseUrl() . '/images/friendica-64.png';
$email = $a->config['admin_email'];
$closed = (($a->config['register_policy'] == REGISTER_CLOSED) ? 'true' : 'false');
$private = ((Config::get('system', 'block_public')) ? 'true' : 'false');
$textlimit = (string) (($a->config['max_import_size']) ? $a->config['max_import_size'] : 200000);
if ($a->config['api_import_size']) {
$textlimit = (string) $a->config['api_import_size'];
}
$ssl = ((Config::get('system', 'have_ssl')) ? 'true' : 'false');
$sslserver = (($ssl === 'true') ? str_replace('http:', 'https:', System::baseUrl()) : '');
$config = [
'site' => ['name' => $name,'server' => $server, 'theme' => 'default', 'path' => '',
'logo' => $logo, 'fancy' => true, 'language' => 'en', 'email' => $email, 'broughtby' => '',
'broughtbyurl' => '', 'timezone' => 'UTC', 'closed' => $closed, 'inviteonly' => false,
'private' => $private, 'textlimit' => $textlimit, 'sslserver' => $sslserver, 'ssl' => $ssl,
'shorturllength' => '30',
'friendica' => [
'FRIENDICA_PLATFORM' => FRIENDICA_PLATFORM,
'FRIENDICA_VERSION' => FRIENDICA_VERSION,
'DFRN_PROTOCOL_VERSION' => DFRN_PROTOCOL_VERSION,
'DB_UPDATE_VERSION' => DB_UPDATE_VERSION
]
],
];
return api_format_data('config', $type, ['config' => $config]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/gnusocial/config', 'api_statusnet_config', false);
api_register_func('api/statusnet/config', 'api_statusnet_config', false);
/**
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
*/
function api_statusnet_version($type)
{
// liar
$fake_statusnet_version = "0.9.7";
return api_format_data('version', $type, ['version' => $fake_statusnet_version]);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/gnusocial/version', 'api_statusnet_version', false);
api_register_func('api/statusnet/version', 'api_statusnet_version', false);
/**
*
* @param string $type Return type (atom, rss, xml, json)
*
* @todo use api_format_data() to return data
*/
function api_ff_ids($type)
{
if (! api_user()) {
throw new ForbiddenException();
}
$a = get_app();
api_get_user($a);
$stringify_ids = defaults($_REQUEST, 'stringify_ids', false);
$r = q(
"SELECT `pcontact`.`id` FROM `contact`
INNER JOIN `contact` AS `pcontact` ON `contact`.`nurl` = `pcontact`.`nurl` AND `pcontact`.`uid` = 0
WHERE `contact`.`uid` = %s AND NOT `contact`.`self`",
intval(api_user())
);
if (!DBM::is_result($r)) {
return;
}
$ids = [];
foreach ($r as $rr) {
if ($stringify_ids) {
$ids[] = $rr['id'];
} else {
$ids[] = intval($rr['id']);
}
}
return api_format_data("ids", $type, ['id' => $ids]);
}
/**
* Returns the ID of every user the user is following.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-ids
*/
function api_friends_ids($type)
{
return api_ff_ids($type);
}
/**
* Returns the ID of every user following the user.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids
*/
function api_followers_ids($type)
{
return api_ff_ids($type);
}
/// @TODO move to top of file or somewhere better
api_register_func('api/friends/ids', 'api_friends_ids', true);
api_register_func('api/followers/ids', 'api_followers_ids', true);
/**
* Sends a new direct message.
*
* @param string $type Return type (atom, rss, xml, json)
*
* @return array|string
* @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-message
*/
function api_direct_messages_new($type)
{
$a = get_app();
if (api_user() === false) {
throw new ForbiddenException();
}
if (!x($_POST, "text") || (!x($_POST, "screen_name") && !x($_POST, "user_id"))) {
return;
}
$sender = api_get_user($a);
if ($_POST['screen_name']) {
$r = q(
"SELECT `id`, `nurl`, `network` FROM `contact` WHERE `uid`=%d AND `nick`='%s'",
intval(api_user()),
dbesc($_POST['screen_name'])
);
// Selecting the id by priority, friendica first
api_best_nickname($r);
$recipient = api_get_user($a, $r[0]['nurl']);
} else {
$recipient = api_get_user($a, $_POST['user_id']);
}
$replyto = '';
$sub = '';
if (x($_REQUEST, 'replyto')) {
$r = q(
'SELECT `parent-uri`, `title` FROM `mail` WHERE `uid`=%d AND `id`=%d',
intval(api_user()),
intval($_REQUEST['replyto'])
);
$replyto = $r[0]['parent-uri'];
$sub = $r[0]['title'];
} else {
if (x($_REQUEST, 'title')) {
$sub = $_REQUEST['title'];
} else {
$sub = ((strlen($_POST['text'])>10) ? substr($_POST['text'], 0, 10)."...":$_POST['text']);
}
}
$id = Mail::send($recipient['cid'], $_POST['text'], $sub, $replyto);
if ($id > -1) {
$r = q("SELECT * FROM `mail` WHERE id=%d", intval($id));
$ret = api_format_messages($r[0], $recipient, $sender);
} else {
$ret = ["error"=>$id];
}
$data = ['direct_message'=>$ret];
switch ($type) {